diff --git a/assets/i18n/de.yaml b/assets/i18n/de.yaml index d6b9fd366..78d5fc778 100644 --- a/assets/i18n/de.yaml +++ b/assets/i18n/de.yaml @@ -171,6 +171,380 @@ "multicam.switch_9": "Zu Kamera 9 wechseln" "effect_library.hint": "Doppelklicken, um zum ausgewählten Clip hinzuzufügen" "effect_library.group.builtin": "Integriert" +"effect_library.group.color": "Color" +"effect_library.group.filter": "Filter" +"effect_library.group.distort": "Distort" +"effect_library.group.keying": "Keying" +"effect_library.group.generator": "Generator" +"effect_library.group.math": "Math" +"effect_library.group.time": "Time" +"effect_library.group.channel": "Channel" +"effect_library.group.transition": "Transition" +"effect_library.group.general": "General" +# Built-in effect (node) and effect-parameter (input) names. The effect +# library and the inspector build these keys at runtime as +# `node..name` and `node..input.`. +"node.polygon.name": "Polygon" +"node.polygon.input.base_in": "Base" +"node.polygon.input.points_in": "Points" +"node.polygon.input.color_in": "Color" +"node.ortho.name": "Orthographic Matrix" +"node.ortho.input.pos_in": "Position" +"node.ortho.input.rot_in": "Rotation" +"node.ortho.input.scale_in": "Scale" +"node.ortho.input.uniform_scale_in": "Uniform Scale" +"node.ortho.input.anchor_in": "Anchor Point" +"node.transform.name": "Transform" +"node.transform.input.tex_in": "Texture" +"node.transform.input.parent_in": "Parent" +"node.transform.input.autoscale_in": "Auto-Scale" +"node.transform.input.interpolation_in": "Interpolation" +"node.transform.input.pos_in": "Position" +"node.transform.input.rot_in": "Rotation" +"node.transform.input.scale_in": "Scale" +"node.transform.input.uniform_scale_in": "Uniform Scale" +"node.transform.input.anchor_in": "Anchor Point" +"node.volume.name": "Volume" +"node.volume.input.samples_in": "Samples" +"node.volume.input.volume_in": "Volume" +"node.pan.name": "Pan" +"node.pan.input.samples_in": "Samples" +"node.pan.input.panning_in": "Pan" +"node.math.name": "Math" +"node.math.input.method_in": "Method" +"node.math.input.param_a_in": "Value" +"node.math.input.param_b_in": "Value" +"node.time.name": "Time" +"node.time.input.enabled_in": "Enabled" +"node.trigonometry.name": "Trigonometry" +"node.trigonometry.input.method_in": "Method" +"node.trigonometry.input.x_in": "Value" +"node.blur.name": "Blur" +"node.blur.input.tex_in": "Input" +"node.blur.input.method_in": "Method" +"node.blur.input.radius_in": "Radius" +"node.blur.input.horiz_in": "Horizontal" +"node.blur.input.vert_in": "Vertical" +"node.blur.input.directional_degrees_in": "Direction" +"node.blur.input.radial_center_in": "Center" +"node.blur.input.repeat_edge_pixels_in": "Repeat Edge Pixels" +"node.solidgenerator.name": "Solid" +"node.solidgenerator.input.color_in": "Color" +"node.merge.name": "Merge" +"node.merge.input.base_in": "Base" +"node.merge.input.blend_in": "Blend" +"node.stroke.name": "Stroke" +"node.stroke.input.tex_in": "Input" +"node.stroke.input.color_in": "Color" +"node.stroke.input.radius_in": "Radius" +"node.stroke.input.opacity_in": "Opacity" +"node.stroke.input.inner_in": "Inner" +"node.textgenerator.name": "Text (Legacy)" +"node.textgenerator.input.text_in": "Text" +"node.textgenerator.input.html_in": "Enable HTML" +"node.textgenerator.input.color_in": "Color" +"node.textgenerator.input.valign_in": "Vertical Align" +"node.textgenerator.input.font_in": "Font" +"node.textgenerator.input.font_size_in": "Font Size" +"node.text2.name": "Text (Legacy)" +"node.text2.input.base_in": "Base" +"node.text2.input.pos_in": "Position" +"node.text2.input.size_in": "Size" +"node.text2.input.color_in": "Color" +"node.text2.input.text_in": "Text" +"node.text2.input.html_in": "Enable HTML" +"node.text2.input.valign_in": "Vertical Align" +"node.text2.input.font_in": "Font" +"node.text2.input.font_size_in": "Font Size" +"node.text3.name": "Text" +"node.text3.input.base_in": "Base" +"node.text3.input.pos_in": "Position" +"node.text3.input.size_in": "Size" +"node.text3.input.plain_text_in": "Text" +"node.text3.input.text_in": "Text" +"node.text3.input.font_family_in": "Font Family" +"node.text3.input.font_size_in": "Font Size" +"node.text3.input.outline_enabled_in": "Outline" +"node.text3.input.outline_color_in": "Outline Color" +"node.text3.input.outline_width_in": "Outline Width" +"node.text3.input.glow_enabled_in": "Glow" +"node.text3.input.glow_color_in": "Glow Color" +"node.text3.input.glow_radius_in": "Glow Radius" +"node.text3.input.color_in": "Color" +"node.text3.input.valign_in": "Vertical Alignment" +"node.text3.input.args_in": "Arguments" +"node.mosaicfilter.name": "Mosaic" +"node.mosaicfilter.input.tex_in": "Texture" +"node.mosaicfilter.input.horiz_in": "Horizontal" +"node.mosaicfilter.input.vert_in": "Vertical" +"node.crop.name": "Crop" +"node.crop.input.tex_in": "Texture" +"node.crop.input.left_in": "Left" +"node.crop.input.top_in": "Top" +"node.crop.input.right_in": "Right" +"node.crop.input.bottom_in": "Bottom" +"node.crop.input.feather_in": "Feather" +"node.value.name": "Value" +"node.value.input.type_in": "Type" +"node.value.input.value_in": "Value" +"node.timeremap.name": "Time Remap" +"node.timeremap.input.time_in": "Time" +"node.timeremap.input.input_in": "Input" +"node.shape.name": "Shape" +"node.shape.input.type_in": "Type" +"node.shape.input.base_in": "Base" +"node.shape.input.pos_in": "Position" +"node.shape.input.size_in": "Size" +"node.shape.input.color_in": "Color" +"node.shape.input.radius_in": "Radius" +"node.colordifferencekey.name": "Color Difference Key" +"node.colordifferencekey.input.tex_in": "Input" +"node.colordifferencekey.input.garbage_in": "Garbage Matte" +"node.colordifferencekey.input.core_in": "Core Matte" +"node.colordifferencekey.input.color_in": "Key Color" +"node.colordifferencekey.input.highlights_in": "Highlights" +"node.colordifferencekey.input.shadows_in": "Shadows" +"node.colordifferencekey.input.mask_only_in": "Show Mask Only" +"node.despill.name": "Despill" +"node.despill.input.tex_in": "Input" +"node.despill.input.color_in": "Key Color" +"node.despill.input.method_in": "Method" +"node.despill.input.preserve_luminance_input": "Preserve Luminance" +"node.group.name": "Group" +"node.opacity.name": "Opacity" +"node.opacity.input.tex_in": "Texture" +"node.opacity.input.opacity_in": "Opacity" +"node.flip.name": "Flip" +"node.flip.input.tex_in": "Input" +"node.flip.input.horiz_in": "Horizontal" +"node.flip.input.vert_in": "Vertical" +"node.noise.name": "Noise" +"node.noise.input.base_in": "Base" +"node.noise.input.strength_in": "Strength" +"node.noise.input.color_in": "Color" +"node.timeoffset.name": "Time Offset" +"node.timeoffset.input.time_in": "Time" +"node.timeoffset.input.input_in": "Input" +"node.transition.name": "Transition" +"node.transition.input.tex_in": "From" +"node.transition.input.blend_in": "To" +"node.transition.input.progress_in": "Progress" +"node.transition.input.type_in": "Type" +"node.transitionfx.name": "Transition FX" +"node.transitionfx.input.tex_in": "From" +"node.transitionfx.input.blend_in": "To" +"node.transitionfx.input.progress_in": "Progress" +"node.transitionfx.input.color_in": "Color" +"node.transitionfx.input.direction_in": "Direction" +"node.transitionfx.input.type_in": "Type" +"node.cornerpin.name": "Corner Pin" +"node.cornerpin.input.tex_in": "Texture" +"node.cornerpin.input.perspective_in": "Perspective" +"node.cornerpin.input.top_left_in": "Top Left" +"node.cornerpin.input.top_right_in": "Top Right" +"node.cornerpin.input.bottom_right_in": "Bottom Right" +"node.cornerpin.input.bottom_left_in": "Bottom Left" +"node.displaytransform.name": "Display Transform" +"node.displaytransform.input.tex_in": "Input" +"node.displaytransform.input.display_in": "Display" +"node.displaytransform.input.view_in": "View" +"node.displaytransform.input.dir_in": "Direction" +"node.ociogradingtransformlinear.name": "OCIO Color Grading (Linear)" +"node.ociogradingtransformlinear.input.tex_in": "Input" +"node.ociogradingtransformlinear.input.ocio_grading_primary_contrast": "Contrast" +"node.ociogradingtransformlinear.input.ocio_grading_primary_offset": "Offset" +"node.ociogradingtransformlinear.input.ocio_grading_primary_exposure": "Exposure" +"node.ociogradingtransformlinear.input.ocio_grading_primary_saturation": "Saturation" +"node.ociogradingtransformlinear.input.ocio_grading_primary_pivot": "Pivot" +"node.ociogradingtransformlinear.input.clamp_black_enable_in": "Enable Black Clamp" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampBlack": "Black Clamp" +"node.ociogradingtransformlinear.input.clamp_white_enable_in": "Enable White Clamp" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampWhite": "White Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.name": "OCIO Color Grading (Log)" +"node.OCIO_NAMESPACEgradingtransformlog.input.tex_in": "Input" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_brightness": "Lift" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_contrast": "Gain" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_gamma": "Gamma" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_saturation": "Saturation" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_pivot": "Pivot" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_black_enable_in": "Enable Black Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampBlack": "Black Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_white_enable_in": "Enable White Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampWhite": "White Clamp" +"node.whitebalance.name": "White Balance" +"node.whitebalance.input.tex_in": "Input" +"node.whitebalance.input.temperature_in": "Temperature (K)" +"node.whitebalance.input.tint_in": "Tint" +"node.ociolut.name": "OCIO LUT" +"node.ociolut.input.tex_in": "Input" +"node.ociolut.input.lut_file_in": "LUT File" +"node.ociolut.input.lut_dir_in": "Direction" +"node.threewaycolor.name": "Three-Way Color" +"node.threewaycolor.input.tex_in": "Input" +"node.threewaycolor.input.shadows_color_in": "Shadows" +"node.threewaycolor.input.midtones_color_in": "Midtones" +"node.threewaycolor.input.highlights_color_in": "Highlights" +"node.threewaycolor.input.shadows_amount_in": "Shadows Amount" +"node.threewaycolor.input.midtones_amount_in": "Midtones Amount" +"node.threewaycolor.input.highlights_amount_in": "Highlights Amount" +"node.chromakey.name": "Chroma Key" +"node.chromakey.input.tex_in": "Input" +"node.chromakey.input.color_key": "Key Color" +"node.chromakey.input.lower_tolerance_in": "Lower Tolerance" +"node.chromakey.input.upper_tolerance_in": "Upper Tolerance" +"node.chromakey.input.garbage_in": "Garbage Matte" +"node.chromakey.input.core_in": "Core Matte" +"node.chromakey.input.highlights_in": "Highlights" +"node.chromakey.input.shadows_in": "Shadows" +"node.chromakey.input.invert_in": "Invert Mask" +"node.chromakey.input.mask_only_in": "Show Mask Only" +"node.mask.name": "Mask" +"node.mask.input.base_in": "Texture" +"node.mask.input.points_in": "Points" +"node.mask.input.color_in": "Color" +"node.mask.input.invert_in": "Invert" +"node.mask.input.feather_in": "Feather" +"node.dropshadow.name": "Drop Shadow" +"node.dropshadow.input.tex_in": "Texture" +"node.dropshadow.input.color_in": "Color" +"node.dropshadow.input.distance_in": "Distance" +"node.dropshadow.input.angle_in": "Angle" +"node.dropshadow.input.radius_in": "Softness" +"node.dropshadow.input.opacity_in": "Opacity" +"node.dropshadow.input.fast_in": "Faster (Lower Quality)" +"node.timeformat.name": "Time Format" +"node.timeformat.input.time_in": "Time" +"node.timeformat.input.format_in": "Format" +"node.timeformat.input.localtime_in": "Interpret time as local time" +"node.wave.name": "Wave" +"node.wave.input.tex_in": "Input" +"node.wave.input.frequency_in": "Frequency" +"node.wave.input.intensity_in": "Intensity" +"node.wave.input.evolution_in": "Evolution" +"node.wave.input.vertical_in": "Direction" +"node.tile.name": "Tile" +"node.tile.input.tex_in": "Input" +"node.tile.input.scale_in": "Scale" +"node.tile.input.position_in": "Position" +"node.tile.input.anchor_in": "Anchor" +"node.tile.input.mirrorx_in": "Mirror Horizontally" +"node.tile.input.mirrory_in": "Mirror Vertically" +"node.swirl.name": "Swirl" +"node.swirl.input.tex_in": "Input" +"node.swirl.input.radius_in": "Radius" +"node.swirl.input.angle_in": "Angle" +"node.swirl.input.pos_in": "Position" +"node.ripple.name": "Ripple" +"node.ripple.input.tex_in": "Input" +"node.ripple.input.evolution_in": "Evolution" +"node.ripple.input.intensity_in": "Intensity" +"node.ripple.input.frequency_in": "Frequency" +"node.ripple.input.position_in": "Position" +"node.ripple.input.stretch_in": "Stretch" +"node.colormatrix.name": "Color Matrix" +"node.colormatrix.input.tex_in": "Input" +"node.colormatrix.input.m0": "R <- R" +"node.colormatrix.input.m1": "R <- G" +"node.colormatrix.input.m2": "R <- B" +"node.colormatrix.input.m3": "R <- A" +"node.colormatrix.input.m4": "G <- R" +"node.colormatrix.input.m5": "G <- G" +"node.colormatrix.input.m6": "G <- B" +"node.colormatrix.input.m7": "G <- A" +"node.colormatrix.input.m8": "B <- R" +"node.colormatrix.input.m9": "B <- G" +"node.colormatrix.input.m10": "B <- B" +"node.colormatrix.input.m11": "B <- A" +"node.colormatrix.input.m12": "A <- R" +"node.colormatrix.input.m13": "A <- G" +"node.colormatrix.input.m14": "A <- B" +"node.colormatrix.input.m15": "A <- A" +"node.dilate.name": "Dilate" +"node.dilate.input.tex_in": "Input" +"node.dilate.input.radius_in": "Radius" +"node.edgedetect.name": "Edge Detect" +"node.edgedetect.input.tex_in": "Input" +"node.edgedetect.input.threshold_in": "Threshold" +"node.erode.name": "Erode" +"node.erode.input.tex_in": "Input" +"node.erode.input.radius_in": "Radius" +"node.colorcorrect.name": "Color Correct" +"node.colorcorrect.input.tex_in": "Input" +"node.colorcorrect.input.saturation_in": "Saturation" +"node.colorcorrect.input.contrast_in": "Contrast" +"node.colorcorrect.input.gamma_in": "Gamma" +"node.colorcorrect.input.gain_in": "Gain" +"node.colorcorrect.input.offset_in": "Offset" +"node.gamma.name": "Gamma" +"node.gamma.input.tex_in": "Input" +"node.gamma.input.gamma_in": "Gamma" +"node.saturation.name": "Saturation" +"node.saturation.input.tex_in": "Input" +"node.saturation.input.saturation_in": "Saturation" +"node.invert.name": "Invert" +"node.invert.input.tex_in": "Input" +"node.invert.input.invert_r_in": "Invert Red" +"node.invert.input.invert_g_in": "Invert Green" +"node.invert.input.invert_b_in": "Invert Blue" +"node.invert.input.invert_a_in": "Invert Alpha" +"node.clamp.name": "Clamp" +"node.clamp.input.tex_in": "Input" +"node.clamp.input.min_in": "Min" +"node.clamp.input.max_in": "Max" +"node.grade.name": "Grade" +"node.grade.input.tex_in": "Input" +"node.grade.input.blackpoint_in": "Black Point" +"node.grade.input.whitepoint_in": "White Point" +"node.grade.input.black_in": "Black Output" +"node.grade.input.white_in": "White Output" +"node.grade.input.gamma_in": "Gamma" +"node.dirblur.name": "Directional Blur" +"node.dirblur.input.tex_in": "Input" +"node.dirblur.input.amount_in": "Amount" +"node.dirblur.input.angle_in": "Angle" +"node.sharpen.name": "Sharpen" +"node.sharpen.input.tex_in": "Input" +"node.sharpen.input.amount_in": "Amount" +"node.dissolve.name": "Dissolve" +"node.dissolve.input.tex_in": "Input" +"node.dissolve.input.blend_in": "Blend" +"node.dissolve.input.mix_in": "Mix" +"node.keymix.name": "KeyMix" +"node.keymix.input.tex_in": "Input" +"node.keymix.input.blend_in": "Blend" +"node.keymix.input.mask_in": "Mask" +"node.premult.name": "Premultiply" +"node.premult.input.tex_in": "Input" +"node.premult.input.premult_channel_in": "Channel" +"node.unpremult.name": "Unpremultiply" +"node.unpremult.input.tex_in": "Input" +"node.unpremult.input.unpremult_channel_in": "Channel" +"node.position.name": "Position" +"node.position.input.tex_in": "Input" +"node.position.input.offset_in": "Translate" +"node.mirror.name": "Mirror" +"node.mirror.input.tex_in": "Input" +"node.mirror.input.horizontal_in": "Horizontal (flop)" +"node.mirror.input.vertical_in": "Vertical (flip)" +"node.checkerboard.name": "Checkerboard" +"node.checkerboard.input.base_in": "Base" +"node.checkerboard.input.size_in": "Box Size" +"node.checkerboard.input.color1_in": "Color 1" +"node.checkerboard.input.color2_in": "Color 2" +"node.colorbars.name": "Color Bars" +"node.colorbars.input.base_in": "Base" +"node.colorbars.input.standard_in": "Standard" +"node.ramp.name": "Ramp" +"node.ramp.input.base_in": "Base" +"node.ramp.input.point0_in": "Point 0" +"node.ramp.input.point1_in": "Point 1" +"node.ramp.input.color0_in": "Color 0" +"node.ramp.input.color1_in": "Color 1" +"node.multicam.name": "Multi-Cam" +"node.multicam.input.current_in": "Current" +"node.multicam.input.sources_in": "Sources" +"node.multicam.input.sequence_in": "Sequence" +"node.multicam.input.sequence_type_in": "Sequence Type" "status.ready": "Bereit" "status.cache": "Cache: Aktiviert" "status.proxy.on": "Proxy: Ein" @@ -481,6 +855,8 @@ "node.category.group": "Gruppe" "project.context.new": "Neu" "project.new_sequence": "Neue Sequenz" +"project.add_text_footage": "Add Text Footage" +"timeline.context.add_adjustment_layer": "Add Adjustment Layer" "project.context.reveal_in_finder": "Im Finder anzeigen" "project.context.replace_footage": "Material ersetzen" "project.context.rename": "Umbenennen" diff --git a/assets/i18n/en-US.yaml b/assets/i18n/en-US.yaml index 206d1edfe..e5387a059 100644 --- a/assets/i18n/en-US.yaml +++ b/assets/i18n/en-US.yaml @@ -169,6 +169,380 @@ "multicam.switch_9": "Switch to Camera 9" "effect_library.hint": "Double-click to add to the selected clip" "effect_library.group.builtin": "Built-in" +"effect_library.group.color": "Color" +"effect_library.group.filter": "Filter" +"effect_library.group.distort": "Distort" +"effect_library.group.keying": "Keying" +"effect_library.group.generator": "Generator" +"effect_library.group.math": "Math" +"effect_library.group.time": "Time" +"effect_library.group.channel": "Channel" +"effect_library.group.transition": "Transition" +"effect_library.group.general": "General" +# Built-in effect (node) and effect-parameter (input) names. The effect +# library and the inspector build these keys at runtime as +# `node..name` and `node..input.`. +"node.polygon.name": "Polygon" +"node.polygon.input.base_in": "Base" +"node.polygon.input.points_in": "Points" +"node.polygon.input.color_in": "Color" +"node.ortho.name": "Orthographic Matrix" +"node.ortho.input.pos_in": "Position" +"node.ortho.input.rot_in": "Rotation" +"node.ortho.input.scale_in": "Scale" +"node.ortho.input.uniform_scale_in": "Uniform Scale" +"node.ortho.input.anchor_in": "Anchor Point" +"node.transform.name": "Transform" +"node.transform.input.tex_in": "Texture" +"node.transform.input.parent_in": "Parent" +"node.transform.input.autoscale_in": "Auto-Scale" +"node.transform.input.interpolation_in": "Interpolation" +"node.transform.input.pos_in": "Position" +"node.transform.input.rot_in": "Rotation" +"node.transform.input.scale_in": "Scale" +"node.transform.input.uniform_scale_in": "Uniform Scale" +"node.transform.input.anchor_in": "Anchor Point" +"node.volume.name": "Volume" +"node.volume.input.samples_in": "Samples" +"node.volume.input.volume_in": "Volume" +"node.pan.name": "Pan" +"node.pan.input.samples_in": "Samples" +"node.pan.input.panning_in": "Pan" +"node.math.name": "Math" +"node.math.input.method_in": "Method" +"node.math.input.param_a_in": "Value" +"node.math.input.param_b_in": "Value" +"node.time.name": "Time" +"node.time.input.enabled_in": "Enabled" +"node.trigonometry.name": "Trigonometry" +"node.trigonometry.input.method_in": "Method" +"node.trigonometry.input.x_in": "Value" +"node.blur.name": "Blur" +"node.blur.input.tex_in": "Input" +"node.blur.input.method_in": "Method" +"node.blur.input.radius_in": "Radius" +"node.blur.input.horiz_in": "Horizontal" +"node.blur.input.vert_in": "Vertical" +"node.blur.input.directional_degrees_in": "Direction" +"node.blur.input.radial_center_in": "Center" +"node.blur.input.repeat_edge_pixels_in": "Repeat Edge Pixels" +"node.solidgenerator.name": "Solid" +"node.solidgenerator.input.color_in": "Color" +"node.merge.name": "Merge" +"node.merge.input.base_in": "Base" +"node.merge.input.blend_in": "Blend" +"node.stroke.name": "Stroke" +"node.stroke.input.tex_in": "Input" +"node.stroke.input.color_in": "Color" +"node.stroke.input.radius_in": "Radius" +"node.stroke.input.opacity_in": "Opacity" +"node.stroke.input.inner_in": "Inner" +"node.textgenerator.name": "Text (Legacy)" +"node.textgenerator.input.text_in": "Text" +"node.textgenerator.input.html_in": "Enable HTML" +"node.textgenerator.input.color_in": "Color" +"node.textgenerator.input.valign_in": "Vertical Align" +"node.textgenerator.input.font_in": "Font" +"node.textgenerator.input.font_size_in": "Font Size" +"node.text2.name": "Text (Legacy)" +"node.text2.input.base_in": "Base" +"node.text2.input.pos_in": "Position" +"node.text2.input.size_in": "Size" +"node.text2.input.color_in": "Color" +"node.text2.input.text_in": "Text" +"node.text2.input.html_in": "Enable HTML" +"node.text2.input.valign_in": "Vertical Align" +"node.text2.input.font_in": "Font" +"node.text2.input.font_size_in": "Font Size" +"node.text3.name": "Text" +"node.text3.input.base_in": "Base" +"node.text3.input.pos_in": "Position" +"node.text3.input.size_in": "Size" +"node.text3.input.plain_text_in": "Text" +"node.text3.input.text_in": "Text" +"node.text3.input.font_family_in": "Font Family" +"node.text3.input.font_size_in": "Font Size" +"node.text3.input.outline_enabled_in": "Outline" +"node.text3.input.outline_color_in": "Outline Color" +"node.text3.input.outline_width_in": "Outline Width" +"node.text3.input.glow_enabled_in": "Glow" +"node.text3.input.glow_color_in": "Glow Color" +"node.text3.input.glow_radius_in": "Glow Radius" +"node.text3.input.color_in": "Color" +"node.text3.input.valign_in": "Vertical Alignment" +"node.text3.input.args_in": "Arguments" +"node.mosaicfilter.name": "Mosaic" +"node.mosaicfilter.input.tex_in": "Texture" +"node.mosaicfilter.input.horiz_in": "Horizontal" +"node.mosaicfilter.input.vert_in": "Vertical" +"node.crop.name": "Crop" +"node.crop.input.tex_in": "Texture" +"node.crop.input.left_in": "Left" +"node.crop.input.top_in": "Top" +"node.crop.input.right_in": "Right" +"node.crop.input.bottom_in": "Bottom" +"node.crop.input.feather_in": "Feather" +"node.value.name": "Value" +"node.value.input.type_in": "Type" +"node.value.input.value_in": "Value" +"node.timeremap.name": "Time Remap" +"node.timeremap.input.time_in": "Time" +"node.timeremap.input.input_in": "Input" +"node.shape.name": "Shape" +"node.shape.input.type_in": "Type" +"node.shape.input.base_in": "Base" +"node.shape.input.pos_in": "Position" +"node.shape.input.size_in": "Size" +"node.shape.input.color_in": "Color" +"node.shape.input.radius_in": "Radius" +"node.colordifferencekey.name": "Color Difference Key" +"node.colordifferencekey.input.tex_in": "Input" +"node.colordifferencekey.input.garbage_in": "Garbage Matte" +"node.colordifferencekey.input.core_in": "Core Matte" +"node.colordifferencekey.input.color_in": "Key Color" +"node.colordifferencekey.input.highlights_in": "Highlights" +"node.colordifferencekey.input.shadows_in": "Shadows" +"node.colordifferencekey.input.mask_only_in": "Show Mask Only" +"node.despill.name": "Despill" +"node.despill.input.tex_in": "Input" +"node.despill.input.color_in": "Key Color" +"node.despill.input.method_in": "Method" +"node.despill.input.preserve_luminance_input": "Preserve Luminance" +"node.group.name": "Group" +"node.opacity.name": "Opacity" +"node.opacity.input.tex_in": "Texture" +"node.opacity.input.opacity_in": "Opacity" +"node.flip.name": "Flip" +"node.flip.input.tex_in": "Input" +"node.flip.input.horiz_in": "Horizontal" +"node.flip.input.vert_in": "Vertical" +"node.noise.name": "Noise" +"node.noise.input.base_in": "Base" +"node.noise.input.strength_in": "Strength" +"node.noise.input.color_in": "Color" +"node.timeoffset.name": "Time Offset" +"node.timeoffset.input.time_in": "Time" +"node.timeoffset.input.input_in": "Input" +"node.transition.name": "Transition" +"node.transition.input.tex_in": "From" +"node.transition.input.blend_in": "To" +"node.transition.input.progress_in": "Progress" +"node.transition.input.type_in": "Type" +"node.transitionfx.name": "Transition FX" +"node.transitionfx.input.tex_in": "From" +"node.transitionfx.input.blend_in": "To" +"node.transitionfx.input.progress_in": "Progress" +"node.transitionfx.input.color_in": "Color" +"node.transitionfx.input.direction_in": "Direction" +"node.transitionfx.input.type_in": "Type" +"node.cornerpin.name": "Corner Pin" +"node.cornerpin.input.tex_in": "Texture" +"node.cornerpin.input.perspective_in": "Perspective" +"node.cornerpin.input.top_left_in": "Top Left" +"node.cornerpin.input.top_right_in": "Top Right" +"node.cornerpin.input.bottom_right_in": "Bottom Right" +"node.cornerpin.input.bottom_left_in": "Bottom Left" +"node.displaytransform.name": "Display Transform" +"node.displaytransform.input.tex_in": "Input" +"node.displaytransform.input.display_in": "Display" +"node.displaytransform.input.view_in": "View" +"node.displaytransform.input.dir_in": "Direction" +"node.ociogradingtransformlinear.name": "OCIO Color Grading (Linear)" +"node.ociogradingtransformlinear.input.tex_in": "Input" +"node.ociogradingtransformlinear.input.ocio_grading_primary_contrast": "Contrast" +"node.ociogradingtransformlinear.input.ocio_grading_primary_offset": "Offset" +"node.ociogradingtransformlinear.input.ocio_grading_primary_exposure": "Exposure" +"node.ociogradingtransformlinear.input.ocio_grading_primary_saturation": "Saturation" +"node.ociogradingtransformlinear.input.ocio_grading_primary_pivot": "Pivot" +"node.ociogradingtransformlinear.input.clamp_black_enable_in": "Enable Black Clamp" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampBlack": "Black Clamp" +"node.ociogradingtransformlinear.input.clamp_white_enable_in": "Enable White Clamp" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampWhite": "White Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.name": "OCIO Color Grading (Log)" +"node.OCIO_NAMESPACEgradingtransformlog.input.tex_in": "Input" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_brightness": "Lift" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_contrast": "Gain" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_gamma": "Gamma" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_saturation": "Saturation" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_pivot": "Pivot" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_black_enable_in": "Enable Black Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampBlack": "Black Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_white_enable_in": "Enable White Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampWhite": "White Clamp" +"node.whitebalance.name": "White Balance" +"node.whitebalance.input.tex_in": "Input" +"node.whitebalance.input.temperature_in": "Temperature (K)" +"node.whitebalance.input.tint_in": "Tint" +"node.ociolut.name": "OCIO LUT" +"node.ociolut.input.tex_in": "Input" +"node.ociolut.input.lut_file_in": "LUT File" +"node.ociolut.input.lut_dir_in": "Direction" +"node.threewaycolor.name": "Three-Way Color" +"node.threewaycolor.input.tex_in": "Input" +"node.threewaycolor.input.shadows_color_in": "Shadows" +"node.threewaycolor.input.midtones_color_in": "Midtones" +"node.threewaycolor.input.highlights_color_in": "Highlights" +"node.threewaycolor.input.shadows_amount_in": "Shadows Amount" +"node.threewaycolor.input.midtones_amount_in": "Midtones Amount" +"node.threewaycolor.input.highlights_amount_in": "Highlights Amount" +"node.chromakey.name": "Chroma Key" +"node.chromakey.input.tex_in": "Input" +"node.chromakey.input.color_key": "Key Color" +"node.chromakey.input.lower_tolerance_in": "Lower Tolerance" +"node.chromakey.input.upper_tolerance_in": "Upper Tolerance" +"node.chromakey.input.garbage_in": "Garbage Matte" +"node.chromakey.input.core_in": "Core Matte" +"node.chromakey.input.highlights_in": "Highlights" +"node.chromakey.input.shadows_in": "Shadows" +"node.chromakey.input.invert_in": "Invert Mask" +"node.chromakey.input.mask_only_in": "Show Mask Only" +"node.mask.name": "Mask" +"node.mask.input.base_in": "Texture" +"node.mask.input.points_in": "Points" +"node.mask.input.color_in": "Color" +"node.mask.input.invert_in": "Invert" +"node.mask.input.feather_in": "Feather" +"node.dropshadow.name": "Drop Shadow" +"node.dropshadow.input.tex_in": "Texture" +"node.dropshadow.input.color_in": "Color" +"node.dropshadow.input.distance_in": "Distance" +"node.dropshadow.input.angle_in": "Angle" +"node.dropshadow.input.radius_in": "Softness" +"node.dropshadow.input.opacity_in": "Opacity" +"node.dropshadow.input.fast_in": "Faster (Lower Quality)" +"node.timeformat.name": "Time Format" +"node.timeformat.input.time_in": "Time" +"node.timeformat.input.format_in": "Format" +"node.timeformat.input.localtime_in": "Interpret time as local time" +"node.wave.name": "Wave" +"node.wave.input.tex_in": "Input" +"node.wave.input.frequency_in": "Frequency" +"node.wave.input.intensity_in": "Intensity" +"node.wave.input.evolution_in": "Evolution" +"node.wave.input.vertical_in": "Direction" +"node.tile.name": "Tile" +"node.tile.input.tex_in": "Input" +"node.tile.input.scale_in": "Scale" +"node.tile.input.position_in": "Position" +"node.tile.input.anchor_in": "Anchor" +"node.tile.input.mirrorx_in": "Mirror Horizontally" +"node.tile.input.mirrory_in": "Mirror Vertically" +"node.swirl.name": "Swirl" +"node.swirl.input.tex_in": "Input" +"node.swirl.input.radius_in": "Radius" +"node.swirl.input.angle_in": "Angle" +"node.swirl.input.pos_in": "Position" +"node.ripple.name": "Ripple" +"node.ripple.input.tex_in": "Input" +"node.ripple.input.evolution_in": "Evolution" +"node.ripple.input.intensity_in": "Intensity" +"node.ripple.input.frequency_in": "Frequency" +"node.ripple.input.position_in": "Position" +"node.ripple.input.stretch_in": "Stretch" +"node.colormatrix.name": "Color Matrix" +"node.colormatrix.input.tex_in": "Input" +"node.colormatrix.input.m0": "R <- R" +"node.colormatrix.input.m1": "R <- G" +"node.colormatrix.input.m2": "R <- B" +"node.colormatrix.input.m3": "R <- A" +"node.colormatrix.input.m4": "G <- R" +"node.colormatrix.input.m5": "G <- G" +"node.colormatrix.input.m6": "G <- B" +"node.colormatrix.input.m7": "G <- A" +"node.colormatrix.input.m8": "B <- R" +"node.colormatrix.input.m9": "B <- G" +"node.colormatrix.input.m10": "B <- B" +"node.colormatrix.input.m11": "B <- A" +"node.colormatrix.input.m12": "A <- R" +"node.colormatrix.input.m13": "A <- G" +"node.colormatrix.input.m14": "A <- B" +"node.colormatrix.input.m15": "A <- A" +"node.dilate.name": "Dilate" +"node.dilate.input.tex_in": "Input" +"node.dilate.input.radius_in": "Radius" +"node.edgedetect.name": "Edge Detect" +"node.edgedetect.input.tex_in": "Input" +"node.edgedetect.input.threshold_in": "Threshold" +"node.erode.name": "Erode" +"node.erode.input.tex_in": "Input" +"node.erode.input.radius_in": "Radius" +"node.colorcorrect.name": "Color Correct" +"node.colorcorrect.input.tex_in": "Input" +"node.colorcorrect.input.saturation_in": "Saturation" +"node.colorcorrect.input.contrast_in": "Contrast" +"node.colorcorrect.input.gamma_in": "Gamma" +"node.colorcorrect.input.gain_in": "Gain" +"node.colorcorrect.input.offset_in": "Offset" +"node.gamma.name": "Gamma" +"node.gamma.input.tex_in": "Input" +"node.gamma.input.gamma_in": "Gamma" +"node.saturation.name": "Saturation" +"node.saturation.input.tex_in": "Input" +"node.saturation.input.saturation_in": "Saturation" +"node.invert.name": "Invert" +"node.invert.input.tex_in": "Input" +"node.invert.input.invert_r_in": "Invert Red" +"node.invert.input.invert_g_in": "Invert Green" +"node.invert.input.invert_b_in": "Invert Blue" +"node.invert.input.invert_a_in": "Invert Alpha" +"node.clamp.name": "Clamp" +"node.clamp.input.tex_in": "Input" +"node.clamp.input.min_in": "Min" +"node.clamp.input.max_in": "Max" +"node.grade.name": "Grade" +"node.grade.input.tex_in": "Input" +"node.grade.input.blackpoint_in": "Black Point" +"node.grade.input.whitepoint_in": "White Point" +"node.grade.input.black_in": "Black Output" +"node.grade.input.white_in": "White Output" +"node.grade.input.gamma_in": "Gamma" +"node.dirblur.name": "Directional Blur" +"node.dirblur.input.tex_in": "Input" +"node.dirblur.input.amount_in": "Amount" +"node.dirblur.input.angle_in": "Angle" +"node.sharpen.name": "Sharpen" +"node.sharpen.input.tex_in": "Input" +"node.sharpen.input.amount_in": "Amount" +"node.dissolve.name": "Dissolve" +"node.dissolve.input.tex_in": "Input" +"node.dissolve.input.blend_in": "Blend" +"node.dissolve.input.mix_in": "Mix" +"node.keymix.name": "KeyMix" +"node.keymix.input.tex_in": "Input" +"node.keymix.input.blend_in": "Blend" +"node.keymix.input.mask_in": "Mask" +"node.premult.name": "Premultiply" +"node.premult.input.tex_in": "Input" +"node.premult.input.premult_channel_in": "Channel" +"node.unpremult.name": "Unpremultiply" +"node.unpremult.input.tex_in": "Input" +"node.unpremult.input.unpremult_channel_in": "Channel" +"node.position.name": "Position" +"node.position.input.tex_in": "Input" +"node.position.input.offset_in": "Translate" +"node.mirror.name": "Mirror" +"node.mirror.input.tex_in": "Input" +"node.mirror.input.horizontal_in": "Horizontal (flop)" +"node.mirror.input.vertical_in": "Vertical (flip)" +"node.checkerboard.name": "Checkerboard" +"node.checkerboard.input.base_in": "Base" +"node.checkerboard.input.size_in": "Box Size" +"node.checkerboard.input.color1_in": "Color 1" +"node.checkerboard.input.color2_in": "Color 2" +"node.colorbars.name": "Color Bars" +"node.colorbars.input.base_in": "Base" +"node.colorbars.input.standard_in": "Standard" +"node.ramp.name": "Ramp" +"node.ramp.input.base_in": "Base" +"node.ramp.input.point0_in": "Point 0" +"node.ramp.input.point1_in": "Point 1" +"node.ramp.input.color0_in": "Color 0" +"node.ramp.input.color1_in": "Color 1" +"node.multicam.name": "Multi-Cam" +"node.multicam.input.current_in": "Current" +"node.multicam.input.sources_in": "Sources" +"node.multicam.input.sequence_in": "Sequence" +"node.multicam.input.sequence_type_in": "Sequence Type" "status.ready": "Ready" "status.cache": "Cache: Enabled" "status.proxy.on": "Proxy: On" @@ -479,6 +853,8 @@ "node.category.group": "Group" "project.context.new": "New" "project.new_sequence": "New Sequence" +"project.add_text_footage": "Add Text Footage" +"timeline.context.add_adjustment_layer": "Add Adjustment Layer" "project.context.reveal_in_finder": "Reveal in Finder" "project.context.replace_footage": "Replace Footage" "project.context.rename": "Rename" diff --git a/assets/i18n/es.yaml b/assets/i18n/es.yaml index fb824f7aa..41188c622 100644 --- a/assets/i18n/es.yaml +++ b/assets/i18n/es.yaml @@ -171,6 +171,380 @@ "multicam.switch_9": "Cambiar a cámara 9" "effect_library.hint": "Haz doble clic para añadir al clip seleccionado" "effect_library.group.builtin": "Integradas" +"effect_library.group.color": "Color" +"effect_library.group.filter": "Filter" +"effect_library.group.distort": "Distort" +"effect_library.group.keying": "Keying" +"effect_library.group.generator": "Generator" +"effect_library.group.math": "Math" +"effect_library.group.time": "Time" +"effect_library.group.channel": "Channel" +"effect_library.group.transition": "Transition" +"effect_library.group.general": "General" +# Built-in effect (node) and effect-parameter (input) names. The effect +# library and the inspector build these keys at runtime as +# `node..name` and `node..input.`. +"node.polygon.name": "Polygon" +"node.polygon.input.base_in": "Base" +"node.polygon.input.points_in": "Points" +"node.polygon.input.color_in": "Color" +"node.ortho.name": "Orthographic Matrix" +"node.ortho.input.pos_in": "Position" +"node.ortho.input.rot_in": "Rotation" +"node.ortho.input.scale_in": "Scale" +"node.ortho.input.uniform_scale_in": "Uniform Scale" +"node.ortho.input.anchor_in": "Anchor Point" +"node.transform.name": "Transform" +"node.transform.input.tex_in": "Texture" +"node.transform.input.parent_in": "Parent" +"node.transform.input.autoscale_in": "Auto-Scale" +"node.transform.input.interpolation_in": "Interpolation" +"node.transform.input.pos_in": "Position" +"node.transform.input.rot_in": "Rotation" +"node.transform.input.scale_in": "Scale" +"node.transform.input.uniform_scale_in": "Uniform Scale" +"node.transform.input.anchor_in": "Anchor Point" +"node.volume.name": "Volume" +"node.volume.input.samples_in": "Samples" +"node.volume.input.volume_in": "Volume" +"node.pan.name": "Pan" +"node.pan.input.samples_in": "Samples" +"node.pan.input.panning_in": "Pan" +"node.math.name": "Math" +"node.math.input.method_in": "Method" +"node.math.input.param_a_in": "Value" +"node.math.input.param_b_in": "Value" +"node.time.name": "Time" +"node.time.input.enabled_in": "Enabled" +"node.trigonometry.name": "Trigonometry" +"node.trigonometry.input.method_in": "Method" +"node.trigonometry.input.x_in": "Value" +"node.blur.name": "Blur" +"node.blur.input.tex_in": "Input" +"node.blur.input.method_in": "Method" +"node.blur.input.radius_in": "Radius" +"node.blur.input.horiz_in": "Horizontal" +"node.blur.input.vert_in": "Vertical" +"node.blur.input.directional_degrees_in": "Direction" +"node.blur.input.radial_center_in": "Center" +"node.blur.input.repeat_edge_pixels_in": "Repeat Edge Pixels" +"node.solidgenerator.name": "Solid" +"node.solidgenerator.input.color_in": "Color" +"node.merge.name": "Merge" +"node.merge.input.base_in": "Base" +"node.merge.input.blend_in": "Blend" +"node.stroke.name": "Stroke" +"node.stroke.input.tex_in": "Input" +"node.stroke.input.color_in": "Color" +"node.stroke.input.radius_in": "Radius" +"node.stroke.input.opacity_in": "Opacity" +"node.stroke.input.inner_in": "Inner" +"node.textgenerator.name": "Text (Legacy)" +"node.textgenerator.input.text_in": "Text" +"node.textgenerator.input.html_in": "Enable HTML" +"node.textgenerator.input.color_in": "Color" +"node.textgenerator.input.valign_in": "Vertical Align" +"node.textgenerator.input.font_in": "Font" +"node.textgenerator.input.font_size_in": "Font Size" +"node.text2.name": "Text (Legacy)" +"node.text2.input.base_in": "Base" +"node.text2.input.pos_in": "Position" +"node.text2.input.size_in": "Size" +"node.text2.input.color_in": "Color" +"node.text2.input.text_in": "Text" +"node.text2.input.html_in": "Enable HTML" +"node.text2.input.valign_in": "Vertical Align" +"node.text2.input.font_in": "Font" +"node.text2.input.font_size_in": "Font Size" +"node.text3.name": "Text" +"node.text3.input.base_in": "Base" +"node.text3.input.pos_in": "Position" +"node.text3.input.size_in": "Size" +"node.text3.input.plain_text_in": "Text" +"node.text3.input.text_in": "Text" +"node.text3.input.font_family_in": "Font Family" +"node.text3.input.font_size_in": "Font Size" +"node.text3.input.outline_enabled_in": "Outline" +"node.text3.input.outline_color_in": "Outline Color" +"node.text3.input.outline_width_in": "Outline Width" +"node.text3.input.glow_enabled_in": "Glow" +"node.text3.input.glow_color_in": "Glow Color" +"node.text3.input.glow_radius_in": "Glow Radius" +"node.text3.input.color_in": "Color" +"node.text3.input.valign_in": "Vertical Alignment" +"node.text3.input.args_in": "Arguments" +"node.mosaicfilter.name": "Mosaic" +"node.mosaicfilter.input.tex_in": "Texture" +"node.mosaicfilter.input.horiz_in": "Horizontal" +"node.mosaicfilter.input.vert_in": "Vertical" +"node.crop.name": "Crop" +"node.crop.input.tex_in": "Texture" +"node.crop.input.left_in": "Left" +"node.crop.input.top_in": "Top" +"node.crop.input.right_in": "Right" +"node.crop.input.bottom_in": "Bottom" +"node.crop.input.feather_in": "Feather" +"node.value.name": "Value" +"node.value.input.type_in": "Type" +"node.value.input.value_in": "Value" +"node.timeremap.name": "Time Remap" +"node.timeremap.input.time_in": "Time" +"node.timeremap.input.input_in": "Input" +"node.shape.name": "Shape" +"node.shape.input.type_in": "Type" +"node.shape.input.base_in": "Base" +"node.shape.input.pos_in": "Position" +"node.shape.input.size_in": "Size" +"node.shape.input.color_in": "Color" +"node.shape.input.radius_in": "Radius" +"node.colordifferencekey.name": "Color Difference Key" +"node.colordifferencekey.input.tex_in": "Input" +"node.colordifferencekey.input.garbage_in": "Garbage Matte" +"node.colordifferencekey.input.core_in": "Core Matte" +"node.colordifferencekey.input.color_in": "Key Color" +"node.colordifferencekey.input.highlights_in": "Highlights" +"node.colordifferencekey.input.shadows_in": "Shadows" +"node.colordifferencekey.input.mask_only_in": "Show Mask Only" +"node.despill.name": "Despill" +"node.despill.input.tex_in": "Input" +"node.despill.input.color_in": "Key Color" +"node.despill.input.method_in": "Method" +"node.despill.input.preserve_luminance_input": "Preserve Luminance" +"node.group.name": "Group" +"node.opacity.name": "Opacity" +"node.opacity.input.tex_in": "Texture" +"node.opacity.input.opacity_in": "Opacity" +"node.flip.name": "Flip" +"node.flip.input.tex_in": "Input" +"node.flip.input.horiz_in": "Horizontal" +"node.flip.input.vert_in": "Vertical" +"node.noise.name": "Noise" +"node.noise.input.base_in": "Base" +"node.noise.input.strength_in": "Strength" +"node.noise.input.color_in": "Color" +"node.timeoffset.name": "Time Offset" +"node.timeoffset.input.time_in": "Time" +"node.timeoffset.input.input_in": "Input" +"node.transition.name": "Transition" +"node.transition.input.tex_in": "From" +"node.transition.input.blend_in": "To" +"node.transition.input.progress_in": "Progress" +"node.transition.input.type_in": "Type" +"node.transitionfx.name": "Transition FX" +"node.transitionfx.input.tex_in": "From" +"node.transitionfx.input.blend_in": "To" +"node.transitionfx.input.progress_in": "Progress" +"node.transitionfx.input.color_in": "Color" +"node.transitionfx.input.direction_in": "Direction" +"node.transitionfx.input.type_in": "Type" +"node.cornerpin.name": "Corner Pin" +"node.cornerpin.input.tex_in": "Texture" +"node.cornerpin.input.perspective_in": "Perspective" +"node.cornerpin.input.top_left_in": "Top Left" +"node.cornerpin.input.top_right_in": "Top Right" +"node.cornerpin.input.bottom_right_in": "Bottom Right" +"node.cornerpin.input.bottom_left_in": "Bottom Left" +"node.displaytransform.name": "Display Transform" +"node.displaytransform.input.tex_in": "Input" +"node.displaytransform.input.display_in": "Display" +"node.displaytransform.input.view_in": "View" +"node.displaytransform.input.dir_in": "Direction" +"node.ociogradingtransformlinear.name": "OCIO Color Grading (Linear)" +"node.ociogradingtransformlinear.input.tex_in": "Input" +"node.ociogradingtransformlinear.input.ocio_grading_primary_contrast": "Contrast" +"node.ociogradingtransformlinear.input.ocio_grading_primary_offset": "Offset" +"node.ociogradingtransformlinear.input.ocio_grading_primary_exposure": "Exposure" +"node.ociogradingtransformlinear.input.ocio_grading_primary_saturation": "Saturation" +"node.ociogradingtransformlinear.input.ocio_grading_primary_pivot": "Pivot" +"node.ociogradingtransformlinear.input.clamp_black_enable_in": "Enable Black Clamp" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampBlack": "Black Clamp" +"node.ociogradingtransformlinear.input.clamp_white_enable_in": "Enable White Clamp" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampWhite": "White Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.name": "OCIO Color Grading (Log)" +"node.OCIO_NAMESPACEgradingtransformlog.input.tex_in": "Input" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_brightness": "Lift" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_contrast": "Gain" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_gamma": "Gamma" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_saturation": "Saturation" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_pivot": "Pivot" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_black_enable_in": "Enable Black Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampBlack": "Black Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_white_enable_in": "Enable White Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampWhite": "White Clamp" +"node.whitebalance.name": "White Balance" +"node.whitebalance.input.tex_in": "Input" +"node.whitebalance.input.temperature_in": "Temperature (K)" +"node.whitebalance.input.tint_in": "Tint" +"node.ociolut.name": "OCIO LUT" +"node.ociolut.input.tex_in": "Input" +"node.ociolut.input.lut_file_in": "LUT File" +"node.ociolut.input.lut_dir_in": "Direction" +"node.threewaycolor.name": "Three-Way Color" +"node.threewaycolor.input.tex_in": "Input" +"node.threewaycolor.input.shadows_color_in": "Shadows" +"node.threewaycolor.input.midtones_color_in": "Midtones" +"node.threewaycolor.input.highlights_color_in": "Highlights" +"node.threewaycolor.input.shadows_amount_in": "Shadows Amount" +"node.threewaycolor.input.midtones_amount_in": "Midtones Amount" +"node.threewaycolor.input.highlights_amount_in": "Highlights Amount" +"node.chromakey.name": "Chroma Key" +"node.chromakey.input.tex_in": "Input" +"node.chromakey.input.color_key": "Key Color" +"node.chromakey.input.lower_tolerance_in": "Lower Tolerance" +"node.chromakey.input.upper_tolerance_in": "Upper Tolerance" +"node.chromakey.input.garbage_in": "Garbage Matte" +"node.chromakey.input.core_in": "Core Matte" +"node.chromakey.input.highlights_in": "Highlights" +"node.chromakey.input.shadows_in": "Shadows" +"node.chromakey.input.invert_in": "Invert Mask" +"node.chromakey.input.mask_only_in": "Show Mask Only" +"node.mask.name": "Mask" +"node.mask.input.base_in": "Texture" +"node.mask.input.points_in": "Points" +"node.mask.input.color_in": "Color" +"node.mask.input.invert_in": "Invert" +"node.mask.input.feather_in": "Feather" +"node.dropshadow.name": "Drop Shadow" +"node.dropshadow.input.tex_in": "Texture" +"node.dropshadow.input.color_in": "Color" +"node.dropshadow.input.distance_in": "Distance" +"node.dropshadow.input.angle_in": "Angle" +"node.dropshadow.input.radius_in": "Softness" +"node.dropshadow.input.opacity_in": "Opacity" +"node.dropshadow.input.fast_in": "Faster (Lower Quality)" +"node.timeformat.name": "Time Format" +"node.timeformat.input.time_in": "Time" +"node.timeformat.input.format_in": "Format" +"node.timeformat.input.localtime_in": "Interpret time as local time" +"node.wave.name": "Wave" +"node.wave.input.tex_in": "Input" +"node.wave.input.frequency_in": "Frequency" +"node.wave.input.intensity_in": "Intensity" +"node.wave.input.evolution_in": "Evolution" +"node.wave.input.vertical_in": "Direction" +"node.tile.name": "Tile" +"node.tile.input.tex_in": "Input" +"node.tile.input.scale_in": "Scale" +"node.tile.input.position_in": "Position" +"node.tile.input.anchor_in": "Anchor" +"node.tile.input.mirrorx_in": "Mirror Horizontally" +"node.tile.input.mirrory_in": "Mirror Vertically" +"node.swirl.name": "Swirl" +"node.swirl.input.tex_in": "Input" +"node.swirl.input.radius_in": "Radius" +"node.swirl.input.angle_in": "Angle" +"node.swirl.input.pos_in": "Position" +"node.ripple.name": "Ripple" +"node.ripple.input.tex_in": "Input" +"node.ripple.input.evolution_in": "Evolution" +"node.ripple.input.intensity_in": "Intensity" +"node.ripple.input.frequency_in": "Frequency" +"node.ripple.input.position_in": "Position" +"node.ripple.input.stretch_in": "Stretch" +"node.colormatrix.name": "Color Matrix" +"node.colormatrix.input.tex_in": "Input" +"node.colormatrix.input.m0": "R <- R" +"node.colormatrix.input.m1": "R <- G" +"node.colormatrix.input.m2": "R <- B" +"node.colormatrix.input.m3": "R <- A" +"node.colormatrix.input.m4": "G <- R" +"node.colormatrix.input.m5": "G <- G" +"node.colormatrix.input.m6": "G <- B" +"node.colormatrix.input.m7": "G <- A" +"node.colormatrix.input.m8": "B <- R" +"node.colormatrix.input.m9": "B <- G" +"node.colormatrix.input.m10": "B <- B" +"node.colormatrix.input.m11": "B <- A" +"node.colormatrix.input.m12": "A <- R" +"node.colormatrix.input.m13": "A <- G" +"node.colormatrix.input.m14": "A <- B" +"node.colormatrix.input.m15": "A <- A" +"node.dilate.name": "Dilate" +"node.dilate.input.tex_in": "Input" +"node.dilate.input.radius_in": "Radius" +"node.edgedetect.name": "Edge Detect" +"node.edgedetect.input.tex_in": "Input" +"node.edgedetect.input.threshold_in": "Threshold" +"node.erode.name": "Erode" +"node.erode.input.tex_in": "Input" +"node.erode.input.radius_in": "Radius" +"node.colorcorrect.name": "Color Correct" +"node.colorcorrect.input.tex_in": "Input" +"node.colorcorrect.input.saturation_in": "Saturation" +"node.colorcorrect.input.contrast_in": "Contrast" +"node.colorcorrect.input.gamma_in": "Gamma" +"node.colorcorrect.input.gain_in": "Gain" +"node.colorcorrect.input.offset_in": "Offset" +"node.gamma.name": "Gamma" +"node.gamma.input.tex_in": "Input" +"node.gamma.input.gamma_in": "Gamma" +"node.saturation.name": "Saturation" +"node.saturation.input.tex_in": "Input" +"node.saturation.input.saturation_in": "Saturation" +"node.invert.name": "Invert" +"node.invert.input.tex_in": "Input" +"node.invert.input.invert_r_in": "Invert Red" +"node.invert.input.invert_g_in": "Invert Green" +"node.invert.input.invert_b_in": "Invert Blue" +"node.invert.input.invert_a_in": "Invert Alpha" +"node.clamp.name": "Clamp" +"node.clamp.input.tex_in": "Input" +"node.clamp.input.min_in": "Min" +"node.clamp.input.max_in": "Max" +"node.grade.name": "Grade" +"node.grade.input.tex_in": "Input" +"node.grade.input.blackpoint_in": "Black Point" +"node.grade.input.whitepoint_in": "White Point" +"node.grade.input.black_in": "Black Output" +"node.grade.input.white_in": "White Output" +"node.grade.input.gamma_in": "Gamma" +"node.dirblur.name": "Directional Blur" +"node.dirblur.input.tex_in": "Input" +"node.dirblur.input.amount_in": "Amount" +"node.dirblur.input.angle_in": "Angle" +"node.sharpen.name": "Sharpen" +"node.sharpen.input.tex_in": "Input" +"node.sharpen.input.amount_in": "Amount" +"node.dissolve.name": "Dissolve" +"node.dissolve.input.tex_in": "Input" +"node.dissolve.input.blend_in": "Blend" +"node.dissolve.input.mix_in": "Mix" +"node.keymix.name": "KeyMix" +"node.keymix.input.tex_in": "Input" +"node.keymix.input.blend_in": "Blend" +"node.keymix.input.mask_in": "Mask" +"node.premult.name": "Premultiply" +"node.premult.input.tex_in": "Input" +"node.premult.input.premult_channel_in": "Channel" +"node.unpremult.name": "Unpremultiply" +"node.unpremult.input.tex_in": "Input" +"node.unpremult.input.unpremult_channel_in": "Channel" +"node.position.name": "Position" +"node.position.input.tex_in": "Input" +"node.position.input.offset_in": "Translate" +"node.mirror.name": "Mirror" +"node.mirror.input.tex_in": "Input" +"node.mirror.input.horizontal_in": "Horizontal (flop)" +"node.mirror.input.vertical_in": "Vertical (flip)" +"node.checkerboard.name": "Checkerboard" +"node.checkerboard.input.base_in": "Base" +"node.checkerboard.input.size_in": "Box Size" +"node.checkerboard.input.color1_in": "Color 1" +"node.checkerboard.input.color2_in": "Color 2" +"node.colorbars.name": "Color Bars" +"node.colorbars.input.base_in": "Base" +"node.colorbars.input.standard_in": "Standard" +"node.ramp.name": "Ramp" +"node.ramp.input.base_in": "Base" +"node.ramp.input.point0_in": "Point 0" +"node.ramp.input.point1_in": "Point 1" +"node.ramp.input.color0_in": "Color 0" +"node.ramp.input.color1_in": "Color 1" +"node.multicam.name": "Multi-Cam" +"node.multicam.input.current_in": "Current" +"node.multicam.input.sources_in": "Sources" +"node.multicam.input.sequence_in": "Sequence" +"node.multicam.input.sequence_type_in": "Sequence Type" "status.ready": "Listo" "status.cache": "Caché: activada" "status.proxy.on": "Proxy: activado" @@ -481,6 +855,8 @@ "node.category.group": "Grupo" "project.context.new": "Nuevo" "project.new_sequence": "Nueva secuencia" +"project.add_text_footage": "Add Text Footage" +"timeline.context.add_adjustment_layer": "Add Adjustment Layer" "project.context.reveal_in_finder": "Mostrar en Finder" "project.context.replace_footage": "Reemplazar material" "project.context.rename": "Cambiar nombre" diff --git a/assets/i18n/fr.yaml b/assets/i18n/fr.yaml index f89f70ed3..6c7355a81 100644 --- a/assets/i18n/fr.yaml +++ b/assets/i18n/fr.yaml @@ -171,6 +171,380 @@ "multicam.switch_9": "Passer à la caméra 9" "effect_library.hint": "Double-cliquer pour ajouter au clip sélectionné" "effect_library.group.builtin": "Intégré" +"effect_library.group.color": "Color" +"effect_library.group.filter": "Filter" +"effect_library.group.distort": "Distort" +"effect_library.group.keying": "Keying" +"effect_library.group.generator": "Generator" +"effect_library.group.math": "Math" +"effect_library.group.time": "Time" +"effect_library.group.channel": "Channel" +"effect_library.group.transition": "Transition" +"effect_library.group.general": "General" +# Built-in effect (node) and effect-parameter (input) names. The effect +# library and the inspector build these keys at runtime as +# `node..name` and `node..input.`. +"node.polygon.name": "Polygon" +"node.polygon.input.base_in": "Base" +"node.polygon.input.points_in": "Points" +"node.polygon.input.color_in": "Color" +"node.ortho.name": "Orthographic Matrix" +"node.ortho.input.pos_in": "Position" +"node.ortho.input.rot_in": "Rotation" +"node.ortho.input.scale_in": "Scale" +"node.ortho.input.uniform_scale_in": "Uniform Scale" +"node.ortho.input.anchor_in": "Anchor Point" +"node.transform.name": "Transform" +"node.transform.input.tex_in": "Texture" +"node.transform.input.parent_in": "Parent" +"node.transform.input.autoscale_in": "Auto-Scale" +"node.transform.input.interpolation_in": "Interpolation" +"node.transform.input.pos_in": "Position" +"node.transform.input.rot_in": "Rotation" +"node.transform.input.scale_in": "Scale" +"node.transform.input.uniform_scale_in": "Uniform Scale" +"node.transform.input.anchor_in": "Anchor Point" +"node.volume.name": "Volume" +"node.volume.input.samples_in": "Samples" +"node.volume.input.volume_in": "Volume" +"node.pan.name": "Pan" +"node.pan.input.samples_in": "Samples" +"node.pan.input.panning_in": "Pan" +"node.math.name": "Math" +"node.math.input.method_in": "Method" +"node.math.input.param_a_in": "Value" +"node.math.input.param_b_in": "Value" +"node.time.name": "Time" +"node.time.input.enabled_in": "Enabled" +"node.trigonometry.name": "Trigonometry" +"node.trigonometry.input.method_in": "Method" +"node.trigonometry.input.x_in": "Value" +"node.blur.name": "Blur" +"node.blur.input.tex_in": "Input" +"node.blur.input.method_in": "Method" +"node.blur.input.radius_in": "Radius" +"node.blur.input.horiz_in": "Horizontal" +"node.blur.input.vert_in": "Vertical" +"node.blur.input.directional_degrees_in": "Direction" +"node.blur.input.radial_center_in": "Center" +"node.blur.input.repeat_edge_pixels_in": "Repeat Edge Pixels" +"node.solidgenerator.name": "Solid" +"node.solidgenerator.input.color_in": "Color" +"node.merge.name": "Merge" +"node.merge.input.base_in": "Base" +"node.merge.input.blend_in": "Blend" +"node.stroke.name": "Stroke" +"node.stroke.input.tex_in": "Input" +"node.stroke.input.color_in": "Color" +"node.stroke.input.radius_in": "Radius" +"node.stroke.input.opacity_in": "Opacity" +"node.stroke.input.inner_in": "Inner" +"node.textgenerator.name": "Text (Legacy)" +"node.textgenerator.input.text_in": "Text" +"node.textgenerator.input.html_in": "Enable HTML" +"node.textgenerator.input.color_in": "Color" +"node.textgenerator.input.valign_in": "Vertical Align" +"node.textgenerator.input.font_in": "Font" +"node.textgenerator.input.font_size_in": "Font Size" +"node.text2.name": "Text (Legacy)" +"node.text2.input.base_in": "Base" +"node.text2.input.pos_in": "Position" +"node.text2.input.size_in": "Size" +"node.text2.input.color_in": "Color" +"node.text2.input.text_in": "Text" +"node.text2.input.html_in": "Enable HTML" +"node.text2.input.valign_in": "Vertical Align" +"node.text2.input.font_in": "Font" +"node.text2.input.font_size_in": "Font Size" +"node.text3.name": "Text" +"node.text3.input.base_in": "Base" +"node.text3.input.pos_in": "Position" +"node.text3.input.size_in": "Size" +"node.text3.input.plain_text_in": "Text" +"node.text3.input.text_in": "Text" +"node.text3.input.font_family_in": "Font Family" +"node.text3.input.font_size_in": "Font Size" +"node.text3.input.outline_enabled_in": "Outline" +"node.text3.input.outline_color_in": "Outline Color" +"node.text3.input.outline_width_in": "Outline Width" +"node.text3.input.glow_enabled_in": "Glow" +"node.text3.input.glow_color_in": "Glow Color" +"node.text3.input.glow_radius_in": "Glow Radius" +"node.text3.input.color_in": "Color" +"node.text3.input.valign_in": "Vertical Alignment" +"node.text3.input.args_in": "Arguments" +"node.mosaicfilter.name": "Mosaic" +"node.mosaicfilter.input.tex_in": "Texture" +"node.mosaicfilter.input.horiz_in": "Horizontal" +"node.mosaicfilter.input.vert_in": "Vertical" +"node.crop.name": "Crop" +"node.crop.input.tex_in": "Texture" +"node.crop.input.left_in": "Left" +"node.crop.input.top_in": "Top" +"node.crop.input.right_in": "Right" +"node.crop.input.bottom_in": "Bottom" +"node.crop.input.feather_in": "Feather" +"node.value.name": "Value" +"node.value.input.type_in": "Type" +"node.value.input.value_in": "Value" +"node.timeremap.name": "Time Remap" +"node.timeremap.input.time_in": "Time" +"node.timeremap.input.input_in": "Input" +"node.shape.name": "Shape" +"node.shape.input.type_in": "Type" +"node.shape.input.base_in": "Base" +"node.shape.input.pos_in": "Position" +"node.shape.input.size_in": "Size" +"node.shape.input.color_in": "Color" +"node.shape.input.radius_in": "Radius" +"node.colordifferencekey.name": "Color Difference Key" +"node.colordifferencekey.input.tex_in": "Input" +"node.colordifferencekey.input.garbage_in": "Garbage Matte" +"node.colordifferencekey.input.core_in": "Core Matte" +"node.colordifferencekey.input.color_in": "Key Color" +"node.colordifferencekey.input.highlights_in": "Highlights" +"node.colordifferencekey.input.shadows_in": "Shadows" +"node.colordifferencekey.input.mask_only_in": "Show Mask Only" +"node.despill.name": "Despill" +"node.despill.input.tex_in": "Input" +"node.despill.input.color_in": "Key Color" +"node.despill.input.method_in": "Method" +"node.despill.input.preserve_luminance_input": "Preserve Luminance" +"node.group.name": "Group" +"node.opacity.name": "Opacity" +"node.opacity.input.tex_in": "Texture" +"node.opacity.input.opacity_in": "Opacity" +"node.flip.name": "Flip" +"node.flip.input.tex_in": "Input" +"node.flip.input.horiz_in": "Horizontal" +"node.flip.input.vert_in": "Vertical" +"node.noise.name": "Noise" +"node.noise.input.base_in": "Base" +"node.noise.input.strength_in": "Strength" +"node.noise.input.color_in": "Color" +"node.timeoffset.name": "Time Offset" +"node.timeoffset.input.time_in": "Time" +"node.timeoffset.input.input_in": "Input" +"node.transition.name": "Transition" +"node.transition.input.tex_in": "From" +"node.transition.input.blend_in": "To" +"node.transition.input.progress_in": "Progress" +"node.transition.input.type_in": "Type" +"node.transitionfx.name": "Transition FX" +"node.transitionfx.input.tex_in": "From" +"node.transitionfx.input.blend_in": "To" +"node.transitionfx.input.progress_in": "Progress" +"node.transitionfx.input.color_in": "Color" +"node.transitionfx.input.direction_in": "Direction" +"node.transitionfx.input.type_in": "Type" +"node.cornerpin.name": "Corner Pin" +"node.cornerpin.input.tex_in": "Texture" +"node.cornerpin.input.perspective_in": "Perspective" +"node.cornerpin.input.top_left_in": "Top Left" +"node.cornerpin.input.top_right_in": "Top Right" +"node.cornerpin.input.bottom_right_in": "Bottom Right" +"node.cornerpin.input.bottom_left_in": "Bottom Left" +"node.displaytransform.name": "Display Transform" +"node.displaytransform.input.tex_in": "Input" +"node.displaytransform.input.display_in": "Display" +"node.displaytransform.input.view_in": "View" +"node.displaytransform.input.dir_in": "Direction" +"node.ociogradingtransformlinear.name": "OCIO Color Grading (Linear)" +"node.ociogradingtransformlinear.input.tex_in": "Input" +"node.ociogradingtransformlinear.input.ocio_grading_primary_contrast": "Contrast" +"node.ociogradingtransformlinear.input.ocio_grading_primary_offset": "Offset" +"node.ociogradingtransformlinear.input.ocio_grading_primary_exposure": "Exposure" +"node.ociogradingtransformlinear.input.ocio_grading_primary_saturation": "Saturation" +"node.ociogradingtransformlinear.input.ocio_grading_primary_pivot": "Pivot" +"node.ociogradingtransformlinear.input.clamp_black_enable_in": "Enable Black Clamp" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampBlack": "Black Clamp" +"node.ociogradingtransformlinear.input.clamp_white_enable_in": "Enable White Clamp" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampWhite": "White Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.name": "OCIO Color Grading (Log)" +"node.OCIO_NAMESPACEgradingtransformlog.input.tex_in": "Input" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_brightness": "Lift" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_contrast": "Gain" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_gamma": "Gamma" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_saturation": "Saturation" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_pivot": "Pivot" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_black_enable_in": "Enable Black Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampBlack": "Black Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_white_enable_in": "Enable White Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampWhite": "White Clamp" +"node.whitebalance.name": "White Balance" +"node.whitebalance.input.tex_in": "Input" +"node.whitebalance.input.temperature_in": "Temperature (K)" +"node.whitebalance.input.tint_in": "Tint" +"node.ociolut.name": "OCIO LUT" +"node.ociolut.input.tex_in": "Input" +"node.ociolut.input.lut_file_in": "LUT File" +"node.ociolut.input.lut_dir_in": "Direction" +"node.threewaycolor.name": "Three-Way Color" +"node.threewaycolor.input.tex_in": "Input" +"node.threewaycolor.input.shadows_color_in": "Shadows" +"node.threewaycolor.input.midtones_color_in": "Midtones" +"node.threewaycolor.input.highlights_color_in": "Highlights" +"node.threewaycolor.input.shadows_amount_in": "Shadows Amount" +"node.threewaycolor.input.midtones_amount_in": "Midtones Amount" +"node.threewaycolor.input.highlights_amount_in": "Highlights Amount" +"node.chromakey.name": "Chroma Key" +"node.chromakey.input.tex_in": "Input" +"node.chromakey.input.color_key": "Key Color" +"node.chromakey.input.lower_tolerance_in": "Lower Tolerance" +"node.chromakey.input.upper_tolerance_in": "Upper Tolerance" +"node.chromakey.input.garbage_in": "Garbage Matte" +"node.chromakey.input.core_in": "Core Matte" +"node.chromakey.input.highlights_in": "Highlights" +"node.chromakey.input.shadows_in": "Shadows" +"node.chromakey.input.invert_in": "Invert Mask" +"node.chromakey.input.mask_only_in": "Show Mask Only" +"node.mask.name": "Mask" +"node.mask.input.base_in": "Texture" +"node.mask.input.points_in": "Points" +"node.mask.input.color_in": "Color" +"node.mask.input.invert_in": "Invert" +"node.mask.input.feather_in": "Feather" +"node.dropshadow.name": "Drop Shadow" +"node.dropshadow.input.tex_in": "Texture" +"node.dropshadow.input.color_in": "Color" +"node.dropshadow.input.distance_in": "Distance" +"node.dropshadow.input.angle_in": "Angle" +"node.dropshadow.input.radius_in": "Softness" +"node.dropshadow.input.opacity_in": "Opacity" +"node.dropshadow.input.fast_in": "Faster (Lower Quality)" +"node.timeformat.name": "Time Format" +"node.timeformat.input.time_in": "Time" +"node.timeformat.input.format_in": "Format" +"node.timeformat.input.localtime_in": "Interpret time as local time" +"node.wave.name": "Wave" +"node.wave.input.tex_in": "Input" +"node.wave.input.frequency_in": "Frequency" +"node.wave.input.intensity_in": "Intensity" +"node.wave.input.evolution_in": "Evolution" +"node.wave.input.vertical_in": "Direction" +"node.tile.name": "Tile" +"node.tile.input.tex_in": "Input" +"node.tile.input.scale_in": "Scale" +"node.tile.input.position_in": "Position" +"node.tile.input.anchor_in": "Anchor" +"node.tile.input.mirrorx_in": "Mirror Horizontally" +"node.tile.input.mirrory_in": "Mirror Vertically" +"node.swirl.name": "Swirl" +"node.swirl.input.tex_in": "Input" +"node.swirl.input.radius_in": "Radius" +"node.swirl.input.angle_in": "Angle" +"node.swirl.input.pos_in": "Position" +"node.ripple.name": "Ripple" +"node.ripple.input.tex_in": "Input" +"node.ripple.input.evolution_in": "Evolution" +"node.ripple.input.intensity_in": "Intensity" +"node.ripple.input.frequency_in": "Frequency" +"node.ripple.input.position_in": "Position" +"node.ripple.input.stretch_in": "Stretch" +"node.colormatrix.name": "Color Matrix" +"node.colormatrix.input.tex_in": "Input" +"node.colormatrix.input.m0": "R <- R" +"node.colormatrix.input.m1": "R <- G" +"node.colormatrix.input.m2": "R <- B" +"node.colormatrix.input.m3": "R <- A" +"node.colormatrix.input.m4": "G <- R" +"node.colormatrix.input.m5": "G <- G" +"node.colormatrix.input.m6": "G <- B" +"node.colormatrix.input.m7": "G <- A" +"node.colormatrix.input.m8": "B <- R" +"node.colormatrix.input.m9": "B <- G" +"node.colormatrix.input.m10": "B <- B" +"node.colormatrix.input.m11": "B <- A" +"node.colormatrix.input.m12": "A <- R" +"node.colormatrix.input.m13": "A <- G" +"node.colormatrix.input.m14": "A <- B" +"node.colormatrix.input.m15": "A <- A" +"node.dilate.name": "Dilate" +"node.dilate.input.tex_in": "Input" +"node.dilate.input.radius_in": "Radius" +"node.edgedetect.name": "Edge Detect" +"node.edgedetect.input.tex_in": "Input" +"node.edgedetect.input.threshold_in": "Threshold" +"node.erode.name": "Erode" +"node.erode.input.tex_in": "Input" +"node.erode.input.radius_in": "Radius" +"node.colorcorrect.name": "Color Correct" +"node.colorcorrect.input.tex_in": "Input" +"node.colorcorrect.input.saturation_in": "Saturation" +"node.colorcorrect.input.contrast_in": "Contrast" +"node.colorcorrect.input.gamma_in": "Gamma" +"node.colorcorrect.input.gain_in": "Gain" +"node.colorcorrect.input.offset_in": "Offset" +"node.gamma.name": "Gamma" +"node.gamma.input.tex_in": "Input" +"node.gamma.input.gamma_in": "Gamma" +"node.saturation.name": "Saturation" +"node.saturation.input.tex_in": "Input" +"node.saturation.input.saturation_in": "Saturation" +"node.invert.name": "Invert" +"node.invert.input.tex_in": "Input" +"node.invert.input.invert_r_in": "Invert Red" +"node.invert.input.invert_g_in": "Invert Green" +"node.invert.input.invert_b_in": "Invert Blue" +"node.invert.input.invert_a_in": "Invert Alpha" +"node.clamp.name": "Clamp" +"node.clamp.input.tex_in": "Input" +"node.clamp.input.min_in": "Min" +"node.clamp.input.max_in": "Max" +"node.grade.name": "Grade" +"node.grade.input.tex_in": "Input" +"node.grade.input.blackpoint_in": "Black Point" +"node.grade.input.whitepoint_in": "White Point" +"node.grade.input.black_in": "Black Output" +"node.grade.input.white_in": "White Output" +"node.grade.input.gamma_in": "Gamma" +"node.dirblur.name": "Directional Blur" +"node.dirblur.input.tex_in": "Input" +"node.dirblur.input.amount_in": "Amount" +"node.dirblur.input.angle_in": "Angle" +"node.sharpen.name": "Sharpen" +"node.sharpen.input.tex_in": "Input" +"node.sharpen.input.amount_in": "Amount" +"node.dissolve.name": "Dissolve" +"node.dissolve.input.tex_in": "Input" +"node.dissolve.input.blend_in": "Blend" +"node.dissolve.input.mix_in": "Mix" +"node.keymix.name": "KeyMix" +"node.keymix.input.tex_in": "Input" +"node.keymix.input.blend_in": "Blend" +"node.keymix.input.mask_in": "Mask" +"node.premult.name": "Premultiply" +"node.premult.input.tex_in": "Input" +"node.premult.input.premult_channel_in": "Channel" +"node.unpremult.name": "Unpremultiply" +"node.unpremult.input.tex_in": "Input" +"node.unpremult.input.unpremult_channel_in": "Channel" +"node.position.name": "Position" +"node.position.input.tex_in": "Input" +"node.position.input.offset_in": "Translate" +"node.mirror.name": "Mirror" +"node.mirror.input.tex_in": "Input" +"node.mirror.input.horizontal_in": "Horizontal (flop)" +"node.mirror.input.vertical_in": "Vertical (flip)" +"node.checkerboard.name": "Checkerboard" +"node.checkerboard.input.base_in": "Base" +"node.checkerboard.input.size_in": "Box Size" +"node.checkerboard.input.color1_in": "Color 1" +"node.checkerboard.input.color2_in": "Color 2" +"node.colorbars.name": "Color Bars" +"node.colorbars.input.base_in": "Base" +"node.colorbars.input.standard_in": "Standard" +"node.ramp.name": "Ramp" +"node.ramp.input.base_in": "Base" +"node.ramp.input.point0_in": "Point 0" +"node.ramp.input.point1_in": "Point 1" +"node.ramp.input.color0_in": "Color 0" +"node.ramp.input.color1_in": "Color 1" +"node.multicam.name": "Multi-Cam" +"node.multicam.input.current_in": "Current" +"node.multicam.input.sources_in": "Sources" +"node.multicam.input.sequence_in": "Sequence" +"node.multicam.input.sequence_type_in": "Sequence Type" "status.ready": "Prêt" "status.cache": "Cache : activé" "status.proxy.on": "Proxy : activé" @@ -481,6 +855,8 @@ "node.category.group": "Groupe" "project.context.new": "Nouveau" "project.new_sequence": "Nouvelle séquence" +"project.add_text_footage": "Add Text Footage" +"timeline.context.add_adjustment_layer": "Add Adjustment Layer" "project.context.reveal_in_finder": "Révéler dans le Finder" "project.context.replace_footage": "Remplacer les médias" "project.context.rename": "Renommer" diff --git a/assets/i18n/ja.yaml b/assets/i18n/ja.yaml index 5567c6fc4..fd8fa5f7b 100644 --- a/assets/i18n/ja.yaml +++ b/assets/i18n/ja.yaml @@ -171,6 +171,380 @@ "multicam.switch_9": "カメラ9に切り替え" "effect_library.hint": "ダブルクリックで選択中のクリップに追加します" "effect_library.group.builtin": "内蔵" +"effect_library.group.color": "Color" +"effect_library.group.filter": "Filter" +"effect_library.group.distort": "Distort" +"effect_library.group.keying": "Keying" +"effect_library.group.generator": "Generator" +"effect_library.group.math": "Math" +"effect_library.group.time": "Time" +"effect_library.group.channel": "Channel" +"effect_library.group.transition": "Transition" +"effect_library.group.general": "General" +# Built-in effect (node) and effect-parameter (input) names. The effect +# library and the inspector build these keys at runtime as +# `node..name` and `node..input.`. +"node.polygon.name": "Polygon" +"node.polygon.input.base_in": "Base" +"node.polygon.input.points_in": "Points" +"node.polygon.input.color_in": "Color" +"node.ortho.name": "Orthographic Matrix" +"node.ortho.input.pos_in": "Position" +"node.ortho.input.rot_in": "Rotation" +"node.ortho.input.scale_in": "Scale" +"node.ortho.input.uniform_scale_in": "Uniform Scale" +"node.ortho.input.anchor_in": "Anchor Point" +"node.transform.name": "Transform" +"node.transform.input.tex_in": "Texture" +"node.transform.input.parent_in": "Parent" +"node.transform.input.autoscale_in": "Auto-Scale" +"node.transform.input.interpolation_in": "Interpolation" +"node.transform.input.pos_in": "Position" +"node.transform.input.rot_in": "Rotation" +"node.transform.input.scale_in": "Scale" +"node.transform.input.uniform_scale_in": "Uniform Scale" +"node.transform.input.anchor_in": "Anchor Point" +"node.volume.name": "Volume" +"node.volume.input.samples_in": "Samples" +"node.volume.input.volume_in": "Volume" +"node.pan.name": "Pan" +"node.pan.input.samples_in": "Samples" +"node.pan.input.panning_in": "Pan" +"node.math.name": "Math" +"node.math.input.method_in": "Method" +"node.math.input.param_a_in": "Value" +"node.math.input.param_b_in": "Value" +"node.time.name": "Time" +"node.time.input.enabled_in": "Enabled" +"node.trigonometry.name": "Trigonometry" +"node.trigonometry.input.method_in": "Method" +"node.trigonometry.input.x_in": "Value" +"node.blur.name": "Blur" +"node.blur.input.tex_in": "Input" +"node.blur.input.method_in": "Method" +"node.blur.input.radius_in": "Radius" +"node.blur.input.horiz_in": "Horizontal" +"node.blur.input.vert_in": "Vertical" +"node.blur.input.directional_degrees_in": "Direction" +"node.blur.input.radial_center_in": "Center" +"node.blur.input.repeat_edge_pixels_in": "Repeat Edge Pixels" +"node.solidgenerator.name": "Solid" +"node.solidgenerator.input.color_in": "Color" +"node.merge.name": "Merge" +"node.merge.input.base_in": "Base" +"node.merge.input.blend_in": "Blend" +"node.stroke.name": "Stroke" +"node.stroke.input.tex_in": "Input" +"node.stroke.input.color_in": "Color" +"node.stroke.input.radius_in": "Radius" +"node.stroke.input.opacity_in": "Opacity" +"node.stroke.input.inner_in": "Inner" +"node.textgenerator.name": "Text (Legacy)" +"node.textgenerator.input.text_in": "Text" +"node.textgenerator.input.html_in": "Enable HTML" +"node.textgenerator.input.color_in": "Color" +"node.textgenerator.input.valign_in": "Vertical Align" +"node.textgenerator.input.font_in": "Font" +"node.textgenerator.input.font_size_in": "Font Size" +"node.text2.name": "Text (Legacy)" +"node.text2.input.base_in": "Base" +"node.text2.input.pos_in": "Position" +"node.text2.input.size_in": "Size" +"node.text2.input.color_in": "Color" +"node.text2.input.text_in": "Text" +"node.text2.input.html_in": "Enable HTML" +"node.text2.input.valign_in": "Vertical Align" +"node.text2.input.font_in": "Font" +"node.text2.input.font_size_in": "Font Size" +"node.text3.name": "Text" +"node.text3.input.base_in": "Base" +"node.text3.input.pos_in": "Position" +"node.text3.input.size_in": "Size" +"node.text3.input.plain_text_in": "Text" +"node.text3.input.text_in": "Text" +"node.text3.input.font_family_in": "Font Family" +"node.text3.input.font_size_in": "Font Size" +"node.text3.input.outline_enabled_in": "Outline" +"node.text3.input.outline_color_in": "Outline Color" +"node.text3.input.outline_width_in": "Outline Width" +"node.text3.input.glow_enabled_in": "Glow" +"node.text3.input.glow_color_in": "Glow Color" +"node.text3.input.glow_radius_in": "Glow Radius" +"node.text3.input.color_in": "Color" +"node.text3.input.valign_in": "Vertical Alignment" +"node.text3.input.args_in": "Arguments" +"node.mosaicfilter.name": "Mosaic" +"node.mosaicfilter.input.tex_in": "Texture" +"node.mosaicfilter.input.horiz_in": "Horizontal" +"node.mosaicfilter.input.vert_in": "Vertical" +"node.crop.name": "Crop" +"node.crop.input.tex_in": "Texture" +"node.crop.input.left_in": "Left" +"node.crop.input.top_in": "Top" +"node.crop.input.right_in": "Right" +"node.crop.input.bottom_in": "Bottom" +"node.crop.input.feather_in": "Feather" +"node.value.name": "Value" +"node.value.input.type_in": "Type" +"node.value.input.value_in": "Value" +"node.timeremap.name": "Time Remap" +"node.timeremap.input.time_in": "Time" +"node.timeremap.input.input_in": "Input" +"node.shape.name": "Shape" +"node.shape.input.type_in": "Type" +"node.shape.input.base_in": "Base" +"node.shape.input.pos_in": "Position" +"node.shape.input.size_in": "Size" +"node.shape.input.color_in": "Color" +"node.shape.input.radius_in": "Radius" +"node.colordifferencekey.name": "Color Difference Key" +"node.colordifferencekey.input.tex_in": "Input" +"node.colordifferencekey.input.garbage_in": "Garbage Matte" +"node.colordifferencekey.input.core_in": "Core Matte" +"node.colordifferencekey.input.color_in": "Key Color" +"node.colordifferencekey.input.highlights_in": "Highlights" +"node.colordifferencekey.input.shadows_in": "Shadows" +"node.colordifferencekey.input.mask_only_in": "Show Mask Only" +"node.despill.name": "Despill" +"node.despill.input.tex_in": "Input" +"node.despill.input.color_in": "Key Color" +"node.despill.input.method_in": "Method" +"node.despill.input.preserve_luminance_input": "Preserve Luminance" +"node.group.name": "Group" +"node.opacity.name": "Opacity" +"node.opacity.input.tex_in": "Texture" +"node.opacity.input.opacity_in": "Opacity" +"node.flip.name": "Flip" +"node.flip.input.tex_in": "Input" +"node.flip.input.horiz_in": "Horizontal" +"node.flip.input.vert_in": "Vertical" +"node.noise.name": "Noise" +"node.noise.input.base_in": "Base" +"node.noise.input.strength_in": "Strength" +"node.noise.input.color_in": "Color" +"node.timeoffset.name": "Time Offset" +"node.timeoffset.input.time_in": "Time" +"node.timeoffset.input.input_in": "Input" +"node.transition.name": "Transition" +"node.transition.input.tex_in": "From" +"node.transition.input.blend_in": "To" +"node.transition.input.progress_in": "Progress" +"node.transition.input.type_in": "Type" +"node.transitionfx.name": "Transition FX" +"node.transitionfx.input.tex_in": "From" +"node.transitionfx.input.blend_in": "To" +"node.transitionfx.input.progress_in": "Progress" +"node.transitionfx.input.color_in": "Color" +"node.transitionfx.input.direction_in": "Direction" +"node.transitionfx.input.type_in": "Type" +"node.cornerpin.name": "Corner Pin" +"node.cornerpin.input.tex_in": "Texture" +"node.cornerpin.input.perspective_in": "Perspective" +"node.cornerpin.input.top_left_in": "Top Left" +"node.cornerpin.input.top_right_in": "Top Right" +"node.cornerpin.input.bottom_right_in": "Bottom Right" +"node.cornerpin.input.bottom_left_in": "Bottom Left" +"node.displaytransform.name": "Display Transform" +"node.displaytransform.input.tex_in": "Input" +"node.displaytransform.input.display_in": "Display" +"node.displaytransform.input.view_in": "View" +"node.displaytransform.input.dir_in": "Direction" +"node.ociogradingtransformlinear.name": "OCIO Color Grading (Linear)" +"node.ociogradingtransformlinear.input.tex_in": "Input" +"node.ociogradingtransformlinear.input.ocio_grading_primary_contrast": "Contrast" +"node.ociogradingtransformlinear.input.ocio_grading_primary_offset": "Offset" +"node.ociogradingtransformlinear.input.ocio_grading_primary_exposure": "Exposure" +"node.ociogradingtransformlinear.input.ocio_grading_primary_saturation": "Saturation" +"node.ociogradingtransformlinear.input.ocio_grading_primary_pivot": "Pivot" +"node.ociogradingtransformlinear.input.clamp_black_enable_in": "Enable Black Clamp" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampBlack": "Black Clamp" +"node.ociogradingtransformlinear.input.clamp_white_enable_in": "Enable White Clamp" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampWhite": "White Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.name": "OCIO Color Grading (Log)" +"node.OCIO_NAMESPACEgradingtransformlog.input.tex_in": "Input" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_brightness": "Lift" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_contrast": "Gain" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_gamma": "Gamma" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_saturation": "Saturation" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_pivot": "Pivot" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_black_enable_in": "Enable Black Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampBlack": "Black Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_white_enable_in": "Enable White Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampWhite": "White Clamp" +"node.whitebalance.name": "White Balance" +"node.whitebalance.input.tex_in": "Input" +"node.whitebalance.input.temperature_in": "Temperature (K)" +"node.whitebalance.input.tint_in": "Tint" +"node.ociolut.name": "OCIO LUT" +"node.ociolut.input.tex_in": "Input" +"node.ociolut.input.lut_file_in": "LUT File" +"node.ociolut.input.lut_dir_in": "Direction" +"node.threewaycolor.name": "Three-Way Color" +"node.threewaycolor.input.tex_in": "Input" +"node.threewaycolor.input.shadows_color_in": "Shadows" +"node.threewaycolor.input.midtones_color_in": "Midtones" +"node.threewaycolor.input.highlights_color_in": "Highlights" +"node.threewaycolor.input.shadows_amount_in": "Shadows Amount" +"node.threewaycolor.input.midtones_amount_in": "Midtones Amount" +"node.threewaycolor.input.highlights_amount_in": "Highlights Amount" +"node.chromakey.name": "Chroma Key" +"node.chromakey.input.tex_in": "Input" +"node.chromakey.input.color_key": "Key Color" +"node.chromakey.input.lower_tolerance_in": "Lower Tolerance" +"node.chromakey.input.upper_tolerance_in": "Upper Tolerance" +"node.chromakey.input.garbage_in": "Garbage Matte" +"node.chromakey.input.core_in": "Core Matte" +"node.chromakey.input.highlights_in": "Highlights" +"node.chromakey.input.shadows_in": "Shadows" +"node.chromakey.input.invert_in": "Invert Mask" +"node.chromakey.input.mask_only_in": "Show Mask Only" +"node.mask.name": "Mask" +"node.mask.input.base_in": "Texture" +"node.mask.input.points_in": "Points" +"node.mask.input.color_in": "Color" +"node.mask.input.invert_in": "Invert" +"node.mask.input.feather_in": "Feather" +"node.dropshadow.name": "Drop Shadow" +"node.dropshadow.input.tex_in": "Texture" +"node.dropshadow.input.color_in": "Color" +"node.dropshadow.input.distance_in": "Distance" +"node.dropshadow.input.angle_in": "Angle" +"node.dropshadow.input.radius_in": "Softness" +"node.dropshadow.input.opacity_in": "Opacity" +"node.dropshadow.input.fast_in": "Faster (Lower Quality)" +"node.timeformat.name": "Time Format" +"node.timeformat.input.time_in": "Time" +"node.timeformat.input.format_in": "Format" +"node.timeformat.input.localtime_in": "Interpret time as local time" +"node.wave.name": "Wave" +"node.wave.input.tex_in": "Input" +"node.wave.input.frequency_in": "Frequency" +"node.wave.input.intensity_in": "Intensity" +"node.wave.input.evolution_in": "Evolution" +"node.wave.input.vertical_in": "Direction" +"node.tile.name": "Tile" +"node.tile.input.tex_in": "Input" +"node.tile.input.scale_in": "Scale" +"node.tile.input.position_in": "Position" +"node.tile.input.anchor_in": "Anchor" +"node.tile.input.mirrorx_in": "Mirror Horizontally" +"node.tile.input.mirrory_in": "Mirror Vertically" +"node.swirl.name": "Swirl" +"node.swirl.input.tex_in": "Input" +"node.swirl.input.radius_in": "Radius" +"node.swirl.input.angle_in": "Angle" +"node.swirl.input.pos_in": "Position" +"node.ripple.name": "Ripple" +"node.ripple.input.tex_in": "Input" +"node.ripple.input.evolution_in": "Evolution" +"node.ripple.input.intensity_in": "Intensity" +"node.ripple.input.frequency_in": "Frequency" +"node.ripple.input.position_in": "Position" +"node.ripple.input.stretch_in": "Stretch" +"node.colormatrix.name": "Color Matrix" +"node.colormatrix.input.tex_in": "Input" +"node.colormatrix.input.m0": "R <- R" +"node.colormatrix.input.m1": "R <- G" +"node.colormatrix.input.m2": "R <- B" +"node.colormatrix.input.m3": "R <- A" +"node.colormatrix.input.m4": "G <- R" +"node.colormatrix.input.m5": "G <- G" +"node.colormatrix.input.m6": "G <- B" +"node.colormatrix.input.m7": "G <- A" +"node.colormatrix.input.m8": "B <- R" +"node.colormatrix.input.m9": "B <- G" +"node.colormatrix.input.m10": "B <- B" +"node.colormatrix.input.m11": "B <- A" +"node.colormatrix.input.m12": "A <- R" +"node.colormatrix.input.m13": "A <- G" +"node.colormatrix.input.m14": "A <- B" +"node.colormatrix.input.m15": "A <- A" +"node.dilate.name": "Dilate" +"node.dilate.input.tex_in": "Input" +"node.dilate.input.radius_in": "Radius" +"node.edgedetect.name": "Edge Detect" +"node.edgedetect.input.tex_in": "Input" +"node.edgedetect.input.threshold_in": "Threshold" +"node.erode.name": "Erode" +"node.erode.input.tex_in": "Input" +"node.erode.input.radius_in": "Radius" +"node.colorcorrect.name": "Color Correct" +"node.colorcorrect.input.tex_in": "Input" +"node.colorcorrect.input.saturation_in": "Saturation" +"node.colorcorrect.input.contrast_in": "Contrast" +"node.colorcorrect.input.gamma_in": "Gamma" +"node.colorcorrect.input.gain_in": "Gain" +"node.colorcorrect.input.offset_in": "Offset" +"node.gamma.name": "Gamma" +"node.gamma.input.tex_in": "Input" +"node.gamma.input.gamma_in": "Gamma" +"node.saturation.name": "Saturation" +"node.saturation.input.tex_in": "Input" +"node.saturation.input.saturation_in": "Saturation" +"node.invert.name": "Invert" +"node.invert.input.tex_in": "Input" +"node.invert.input.invert_r_in": "Invert Red" +"node.invert.input.invert_g_in": "Invert Green" +"node.invert.input.invert_b_in": "Invert Blue" +"node.invert.input.invert_a_in": "Invert Alpha" +"node.clamp.name": "Clamp" +"node.clamp.input.tex_in": "Input" +"node.clamp.input.min_in": "Min" +"node.clamp.input.max_in": "Max" +"node.grade.name": "Grade" +"node.grade.input.tex_in": "Input" +"node.grade.input.blackpoint_in": "Black Point" +"node.grade.input.whitepoint_in": "White Point" +"node.grade.input.black_in": "Black Output" +"node.grade.input.white_in": "White Output" +"node.grade.input.gamma_in": "Gamma" +"node.dirblur.name": "Directional Blur" +"node.dirblur.input.tex_in": "Input" +"node.dirblur.input.amount_in": "Amount" +"node.dirblur.input.angle_in": "Angle" +"node.sharpen.name": "Sharpen" +"node.sharpen.input.tex_in": "Input" +"node.sharpen.input.amount_in": "Amount" +"node.dissolve.name": "Dissolve" +"node.dissolve.input.tex_in": "Input" +"node.dissolve.input.blend_in": "Blend" +"node.dissolve.input.mix_in": "Mix" +"node.keymix.name": "KeyMix" +"node.keymix.input.tex_in": "Input" +"node.keymix.input.blend_in": "Blend" +"node.keymix.input.mask_in": "Mask" +"node.premult.name": "Premultiply" +"node.premult.input.tex_in": "Input" +"node.premult.input.premult_channel_in": "Channel" +"node.unpremult.name": "Unpremultiply" +"node.unpremult.input.tex_in": "Input" +"node.unpremult.input.unpremult_channel_in": "Channel" +"node.position.name": "Position" +"node.position.input.tex_in": "Input" +"node.position.input.offset_in": "Translate" +"node.mirror.name": "Mirror" +"node.mirror.input.tex_in": "Input" +"node.mirror.input.horizontal_in": "Horizontal (flop)" +"node.mirror.input.vertical_in": "Vertical (flip)" +"node.checkerboard.name": "Checkerboard" +"node.checkerboard.input.base_in": "Base" +"node.checkerboard.input.size_in": "Box Size" +"node.checkerboard.input.color1_in": "Color 1" +"node.checkerboard.input.color2_in": "Color 2" +"node.colorbars.name": "Color Bars" +"node.colorbars.input.base_in": "Base" +"node.colorbars.input.standard_in": "Standard" +"node.ramp.name": "Ramp" +"node.ramp.input.base_in": "Base" +"node.ramp.input.point0_in": "Point 0" +"node.ramp.input.point1_in": "Point 1" +"node.ramp.input.color0_in": "Color 0" +"node.ramp.input.color1_in": "Color 1" +"node.multicam.name": "Multi-Cam" +"node.multicam.input.current_in": "Current" +"node.multicam.input.sources_in": "Sources" +"node.multicam.input.sequence_in": "Sequence" +"node.multicam.input.sequence_type_in": "Sequence Type" "status.ready": "準備完了" "status.cache": "キャッシュ:有効" "status.proxy.on": "プロキシ:オン" @@ -481,6 +855,8 @@ "node.category.group": "グループ" "project.context.new": "新規" "project.new_sequence": "新規シーケンス" +"project.add_text_footage": "Add Text Footage" +"timeline.context.add_adjustment_layer": "Add Adjustment Layer" "project.context.reveal_in_finder": "Finderに表示" "project.context.replace_footage": "素材を置き換え" "project.context.rename": "名前を変更" diff --git a/assets/i18n/pt.yaml b/assets/i18n/pt.yaml index a7188b5ef..11faef4a5 100644 --- a/assets/i18n/pt.yaml +++ b/assets/i18n/pt.yaml @@ -171,6 +171,380 @@ "multicam.switch_9": "Alternar para a câmera 9" "effect_library.hint": "Clique duas vezes para adicionar ao clipe selecionado" "effect_library.group.builtin": "Integrados" +"effect_library.group.color": "Color" +"effect_library.group.filter": "Filter" +"effect_library.group.distort": "Distort" +"effect_library.group.keying": "Keying" +"effect_library.group.generator": "Generator" +"effect_library.group.math": "Math" +"effect_library.group.time": "Time" +"effect_library.group.channel": "Channel" +"effect_library.group.transition": "Transition" +"effect_library.group.general": "General" +# Built-in effect (node) and effect-parameter (input) names. The effect +# library and the inspector build these keys at runtime as +# `node..name` and `node..input.`. +"node.polygon.name": "Polygon" +"node.polygon.input.base_in": "Base" +"node.polygon.input.points_in": "Points" +"node.polygon.input.color_in": "Color" +"node.ortho.name": "Orthographic Matrix" +"node.ortho.input.pos_in": "Position" +"node.ortho.input.rot_in": "Rotation" +"node.ortho.input.scale_in": "Scale" +"node.ortho.input.uniform_scale_in": "Uniform Scale" +"node.ortho.input.anchor_in": "Anchor Point" +"node.transform.name": "Transform" +"node.transform.input.tex_in": "Texture" +"node.transform.input.parent_in": "Parent" +"node.transform.input.autoscale_in": "Auto-Scale" +"node.transform.input.interpolation_in": "Interpolation" +"node.transform.input.pos_in": "Position" +"node.transform.input.rot_in": "Rotation" +"node.transform.input.scale_in": "Scale" +"node.transform.input.uniform_scale_in": "Uniform Scale" +"node.transform.input.anchor_in": "Anchor Point" +"node.volume.name": "Volume" +"node.volume.input.samples_in": "Samples" +"node.volume.input.volume_in": "Volume" +"node.pan.name": "Pan" +"node.pan.input.samples_in": "Samples" +"node.pan.input.panning_in": "Pan" +"node.math.name": "Math" +"node.math.input.method_in": "Method" +"node.math.input.param_a_in": "Value" +"node.math.input.param_b_in": "Value" +"node.time.name": "Time" +"node.time.input.enabled_in": "Enabled" +"node.trigonometry.name": "Trigonometry" +"node.trigonometry.input.method_in": "Method" +"node.trigonometry.input.x_in": "Value" +"node.blur.name": "Blur" +"node.blur.input.tex_in": "Input" +"node.blur.input.method_in": "Method" +"node.blur.input.radius_in": "Radius" +"node.blur.input.horiz_in": "Horizontal" +"node.blur.input.vert_in": "Vertical" +"node.blur.input.directional_degrees_in": "Direction" +"node.blur.input.radial_center_in": "Center" +"node.blur.input.repeat_edge_pixels_in": "Repeat Edge Pixels" +"node.solidgenerator.name": "Solid" +"node.solidgenerator.input.color_in": "Color" +"node.merge.name": "Merge" +"node.merge.input.base_in": "Base" +"node.merge.input.blend_in": "Blend" +"node.stroke.name": "Stroke" +"node.stroke.input.tex_in": "Input" +"node.stroke.input.color_in": "Color" +"node.stroke.input.radius_in": "Radius" +"node.stroke.input.opacity_in": "Opacity" +"node.stroke.input.inner_in": "Inner" +"node.textgenerator.name": "Text (Legacy)" +"node.textgenerator.input.text_in": "Text" +"node.textgenerator.input.html_in": "Enable HTML" +"node.textgenerator.input.color_in": "Color" +"node.textgenerator.input.valign_in": "Vertical Align" +"node.textgenerator.input.font_in": "Font" +"node.textgenerator.input.font_size_in": "Font Size" +"node.text2.name": "Text (Legacy)" +"node.text2.input.base_in": "Base" +"node.text2.input.pos_in": "Position" +"node.text2.input.size_in": "Size" +"node.text2.input.color_in": "Color" +"node.text2.input.text_in": "Text" +"node.text2.input.html_in": "Enable HTML" +"node.text2.input.valign_in": "Vertical Align" +"node.text2.input.font_in": "Font" +"node.text2.input.font_size_in": "Font Size" +"node.text3.name": "Text" +"node.text3.input.base_in": "Base" +"node.text3.input.pos_in": "Position" +"node.text3.input.size_in": "Size" +"node.text3.input.plain_text_in": "Text" +"node.text3.input.text_in": "Text" +"node.text3.input.font_family_in": "Font Family" +"node.text3.input.font_size_in": "Font Size" +"node.text3.input.outline_enabled_in": "Outline" +"node.text3.input.outline_color_in": "Outline Color" +"node.text3.input.outline_width_in": "Outline Width" +"node.text3.input.glow_enabled_in": "Glow" +"node.text3.input.glow_color_in": "Glow Color" +"node.text3.input.glow_radius_in": "Glow Radius" +"node.text3.input.color_in": "Color" +"node.text3.input.valign_in": "Vertical Alignment" +"node.text3.input.args_in": "Arguments" +"node.mosaicfilter.name": "Mosaic" +"node.mosaicfilter.input.tex_in": "Texture" +"node.mosaicfilter.input.horiz_in": "Horizontal" +"node.mosaicfilter.input.vert_in": "Vertical" +"node.crop.name": "Crop" +"node.crop.input.tex_in": "Texture" +"node.crop.input.left_in": "Left" +"node.crop.input.top_in": "Top" +"node.crop.input.right_in": "Right" +"node.crop.input.bottom_in": "Bottom" +"node.crop.input.feather_in": "Feather" +"node.value.name": "Value" +"node.value.input.type_in": "Type" +"node.value.input.value_in": "Value" +"node.timeremap.name": "Time Remap" +"node.timeremap.input.time_in": "Time" +"node.timeremap.input.input_in": "Input" +"node.shape.name": "Shape" +"node.shape.input.type_in": "Type" +"node.shape.input.base_in": "Base" +"node.shape.input.pos_in": "Position" +"node.shape.input.size_in": "Size" +"node.shape.input.color_in": "Color" +"node.shape.input.radius_in": "Radius" +"node.colordifferencekey.name": "Color Difference Key" +"node.colordifferencekey.input.tex_in": "Input" +"node.colordifferencekey.input.garbage_in": "Garbage Matte" +"node.colordifferencekey.input.core_in": "Core Matte" +"node.colordifferencekey.input.color_in": "Key Color" +"node.colordifferencekey.input.highlights_in": "Highlights" +"node.colordifferencekey.input.shadows_in": "Shadows" +"node.colordifferencekey.input.mask_only_in": "Show Mask Only" +"node.despill.name": "Despill" +"node.despill.input.tex_in": "Input" +"node.despill.input.color_in": "Key Color" +"node.despill.input.method_in": "Method" +"node.despill.input.preserve_luminance_input": "Preserve Luminance" +"node.group.name": "Group" +"node.opacity.name": "Opacity" +"node.opacity.input.tex_in": "Texture" +"node.opacity.input.opacity_in": "Opacity" +"node.flip.name": "Flip" +"node.flip.input.tex_in": "Input" +"node.flip.input.horiz_in": "Horizontal" +"node.flip.input.vert_in": "Vertical" +"node.noise.name": "Noise" +"node.noise.input.base_in": "Base" +"node.noise.input.strength_in": "Strength" +"node.noise.input.color_in": "Color" +"node.timeoffset.name": "Time Offset" +"node.timeoffset.input.time_in": "Time" +"node.timeoffset.input.input_in": "Input" +"node.transition.name": "Transition" +"node.transition.input.tex_in": "From" +"node.transition.input.blend_in": "To" +"node.transition.input.progress_in": "Progress" +"node.transition.input.type_in": "Type" +"node.transitionfx.name": "Transition FX" +"node.transitionfx.input.tex_in": "From" +"node.transitionfx.input.blend_in": "To" +"node.transitionfx.input.progress_in": "Progress" +"node.transitionfx.input.color_in": "Color" +"node.transitionfx.input.direction_in": "Direction" +"node.transitionfx.input.type_in": "Type" +"node.cornerpin.name": "Corner Pin" +"node.cornerpin.input.tex_in": "Texture" +"node.cornerpin.input.perspective_in": "Perspective" +"node.cornerpin.input.top_left_in": "Top Left" +"node.cornerpin.input.top_right_in": "Top Right" +"node.cornerpin.input.bottom_right_in": "Bottom Right" +"node.cornerpin.input.bottom_left_in": "Bottom Left" +"node.displaytransform.name": "Display Transform" +"node.displaytransform.input.tex_in": "Input" +"node.displaytransform.input.display_in": "Display" +"node.displaytransform.input.view_in": "View" +"node.displaytransform.input.dir_in": "Direction" +"node.ociogradingtransformlinear.name": "OCIO Color Grading (Linear)" +"node.ociogradingtransformlinear.input.tex_in": "Input" +"node.ociogradingtransformlinear.input.ocio_grading_primary_contrast": "Contrast" +"node.ociogradingtransformlinear.input.ocio_grading_primary_offset": "Offset" +"node.ociogradingtransformlinear.input.ocio_grading_primary_exposure": "Exposure" +"node.ociogradingtransformlinear.input.ocio_grading_primary_saturation": "Saturation" +"node.ociogradingtransformlinear.input.ocio_grading_primary_pivot": "Pivot" +"node.ociogradingtransformlinear.input.clamp_black_enable_in": "Enable Black Clamp" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampBlack": "Black Clamp" +"node.ociogradingtransformlinear.input.clamp_white_enable_in": "Enable White Clamp" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampWhite": "White Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.name": "OCIO Color Grading (Log)" +"node.OCIO_NAMESPACEgradingtransformlog.input.tex_in": "Input" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_brightness": "Lift" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_contrast": "Gain" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_gamma": "Gamma" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_saturation": "Saturation" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_pivot": "Pivot" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_black_enable_in": "Enable Black Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampBlack": "Black Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_white_enable_in": "Enable White Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampWhite": "White Clamp" +"node.whitebalance.name": "White Balance" +"node.whitebalance.input.tex_in": "Input" +"node.whitebalance.input.temperature_in": "Temperature (K)" +"node.whitebalance.input.tint_in": "Tint" +"node.ociolut.name": "OCIO LUT" +"node.ociolut.input.tex_in": "Input" +"node.ociolut.input.lut_file_in": "LUT File" +"node.ociolut.input.lut_dir_in": "Direction" +"node.threewaycolor.name": "Three-Way Color" +"node.threewaycolor.input.tex_in": "Input" +"node.threewaycolor.input.shadows_color_in": "Shadows" +"node.threewaycolor.input.midtones_color_in": "Midtones" +"node.threewaycolor.input.highlights_color_in": "Highlights" +"node.threewaycolor.input.shadows_amount_in": "Shadows Amount" +"node.threewaycolor.input.midtones_amount_in": "Midtones Amount" +"node.threewaycolor.input.highlights_amount_in": "Highlights Amount" +"node.chromakey.name": "Chroma Key" +"node.chromakey.input.tex_in": "Input" +"node.chromakey.input.color_key": "Key Color" +"node.chromakey.input.lower_tolerance_in": "Lower Tolerance" +"node.chromakey.input.upper_tolerance_in": "Upper Tolerance" +"node.chromakey.input.garbage_in": "Garbage Matte" +"node.chromakey.input.core_in": "Core Matte" +"node.chromakey.input.highlights_in": "Highlights" +"node.chromakey.input.shadows_in": "Shadows" +"node.chromakey.input.invert_in": "Invert Mask" +"node.chromakey.input.mask_only_in": "Show Mask Only" +"node.mask.name": "Mask" +"node.mask.input.base_in": "Texture" +"node.mask.input.points_in": "Points" +"node.mask.input.color_in": "Color" +"node.mask.input.invert_in": "Invert" +"node.mask.input.feather_in": "Feather" +"node.dropshadow.name": "Drop Shadow" +"node.dropshadow.input.tex_in": "Texture" +"node.dropshadow.input.color_in": "Color" +"node.dropshadow.input.distance_in": "Distance" +"node.dropshadow.input.angle_in": "Angle" +"node.dropshadow.input.radius_in": "Softness" +"node.dropshadow.input.opacity_in": "Opacity" +"node.dropshadow.input.fast_in": "Faster (Lower Quality)" +"node.timeformat.name": "Time Format" +"node.timeformat.input.time_in": "Time" +"node.timeformat.input.format_in": "Format" +"node.timeformat.input.localtime_in": "Interpret time as local time" +"node.wave.name": "Wave" +"node.wave.input.tex_in": "Input" +"node.wave.input.frequency_in": "Frequency" +"node.wave.input.intensity_in": "Intensity" +"node.wave.input.evolution_in": "Evolution" +"node.wave.input.vertical_in": "Direction" +"node.tile.name": "Tile" +"node.tile.input.tex_in": "Input" +"node.tile.input.scale_in": "Scale" +"node.tile.input.position_in": "Position" +"node.tile.input.anchor_in": "Anchor" +"node.tile.input.mirrorx_in": "Mirror Horizontally" +"node.tile.input.mirrory_in": "Mirror Vertically" +"node.swirl.name": "Swirl" +"node.swirl.input.tex_in": "Input" +"node.swirl.input.radius_in": "Radius" +"node.swirl.input.angle_in": "Angle" +"node.swirl.input.pos_in": "Position" +"node.ripple.name": "Ripple" +"node.ripple.input.tex_in": "Input" +"node.ripple.input.evolution_in": "Evolution" +"node.ripple.input.intensity_in": "Intensity" +"node.ripple.input.frequency_in": "Frequency" +"node.ripple.input.position_in": "Position" +"node.ripple.input.stretch_in": "Stretch" +"node.colormatrix.name": "Color Matrix" +"node.colormatrix.input.tex_in": "Input" +"node.colormatrix.input.m0": "R <- R" +"node.colormatrix.input.m1": "R <- G" +"node.colormatrix.input.m2": "R <- B" +"node.colormatrix.input.m3": "R <- A" +"node.colormatrix.input.m4": "G <- R" +"node.colormatrix.input.m5": "G <- G" +"node.colormatrix.input.m6": "G <- B" +"node.colormatrix.input.m7": "G <- A" +"node.colormatrix.input.m8": "B <- R" +"node.colormatrix.input.m9": "B <- G" +"node.colormatrix.input.m10": "B <- B" +"node.colormatrix.input.m11": "B <- A" +"node.colormatrix.input.m12": "A <- R" +"node.colormatrix.input.m13": "A <- G" +"node.colormatrix.input.m14": "A <- B" +"node.colormatrix.input.m15": "A <- A" +"node.dilate.name": "Dilate" +"node.dilate.input.tex_in": "Input" +"node.dilate.input.radius_in": "Radius" +"node.edgedetect.name": "Edge Detect" +"node.edgedetect.input.tex_in": "Input" +"node.edgedetect.input.threshold_in": "Threshold" +"node.erode.name": "Erode" +"node.erode.input.tex_in": "Input" +"node.erode.input.radius_in": "Radius" +"node.colorcorrect.name": "Color Correct" +"node.colorcorrect.input.tex_in": "Input" +"node.colorcorrect.input.saturation_in": "Saturation" +"node.colorcorrect.input.contrast_in": "Contrast" +"node.colorcorrect.input.gamma_in": "Gamma" +"node.colorcorrect.input.gain_in": "Gain" +"node.colorcorrect.input.offset_in": "Offset" +"node.gamma.name": "Gamma" +"node.gamma.input.tex_in": "Input" +"node.gamma.input.gamma_in": "Gamma" +"node.saturation.name": "Saturation" +"node.saturation.input.tex_in": "Input" +"node.saturation.input.saturation_in": "Saturation" +"node.invert.name": "Invert" +"node.invert.input.tex_in": "Input" +"node.invert.input.invert_r_in": "Invert Red" +"node.invert.input.invert_g_in": "Invert Green" +"node.invert.input.invert_b_in": "Invert Blue" +"node.invert.input.invert_a_in": "Invert Alpha" +"node.clamp.name": "Clamp" +"node.clamp.input.tex_in": "Input" +"node.clamp.input.min_in": "Min" +"node.clamp.input.max_in": "Max" +"node.grade.name": "Grade" +"node.grade.input.tex_in": "Input" +"node.grade.input.blackpoint_in": "Black Point" +"node.grade.input.whitepoint_in": "White Point" +"node.grade.input.black_in": "Black Output" +"node.grade.input.white_in": "White Output" +"node.grade.input.gamma_in": "Gamma" +"node.dirblur.name": "Directional Blur" +"node.dirblur.input.tex_in": "Input" +"node.dirblur.input.amount_in": "Amount" +"node.dirblur.input.angle_in": "Angle" +"node.sharpen.name": "Sharpen" +"node.sharpen.input.tex_in": "Input" +"node.sharpen.input.amount_in": "Amount" +"node.dissolve.name": "Dissolve" +"node.dissolve.input.tex_in": "Input" +"node.dissolve.input.blend_in": "Blend" +"node.dissolve.input.mix_in": "Mix" +"node.keymix.name": "KeyMix" +"node.keymix.input.tex_in": "Input" +"node.keymix.input.blend_in": "Blend" +"node.keymix.input.mask_in": "Mask" +"node.premult.name": "Premultiply" +"node.premult.input.tex_in": "Input" +"node.premult.input.premult_channel_in": "Channel" +"node.unpremult.name": "Unpremultiply" +"node.unpremult.input.tex_in": "Input" +"node.unpremult.input.unpremult_channel_in": "Channel" +"node.position.name": "Position" +"node.position.input.tex_in": "Input" +"node.position.input.offset_in": "Translate" +"node.mirror.name": "Mirror" +"node.mirror.input.tex_in": "Input" +"node.mirror.input.horizontal_in": "Horizontal (flop)" +"node.mirror.input.vertical_in": "Vertical (flip)" +"node.checkerboard.name": "Checkerboard" +"node.checkerboard.input.base_in": "Base" +"node.checkerboard.input.size_in": "Box Size" +"node.checkerboard.input.color1_in": "Color 1" +"node.checkerboard.input.color2_in": "Color 2" +"node.colorbars.name": "Color Bars" +"node.colorbars.input.base_in": "Base" +"node.colorbars.input.standard_in": "Standard" +"node.ramp.name": "Ramp" +"node.ramp.input.base_in": "Base" +"node.ramp.input.point0_in": "Point 0" +"node.ramp.input.point1_in": "Point 1" +"node.ramp.input.color0_in": "Color 0" +"node.ramp.input.color1_in": "Color 1" +"node.multicam.name": "Multi-Cam" +"node.multicam.input.current_in": "Current" +"node.multicam.input.sources_in": "Sources" +"node.multicam.input.sequence_in": "Sequence" +"node.multicam.input.sequence_type_in": "Sequence Type" "status.ready": "Pronto" "status.cache": "Cache: ativado" "status.proxy.on": "Proxy: ativado" @@ -481,6 +855,8 @@ "node.category.group": "Grupo" "project.context.new": "Novo" "project.new_sequence": "Nova sequência" +"project.add_text_footage": "Add Text Footage" +"timeline.context.add_adjustment_layer": "Add Adjustment Layer" "project.context.reveal_in_finder": "Revelar no Finder" "project.context.replace_footage": "Substituir mídia" "project.context.rename": "Renomear" diff --git a/assets/i18n/ru.yaml b/assets/i18n/ru.yaml index 9d6a58119..fb092621e 100644 --- a/assets/i18n/ru.yaml +++ b/assets/i18n/ru.yaml @@ -171,6 +171,380 @@ "multicam.switch_9": "Переключиться на камеру 9" "effect_library.hint": "Двойной щелчок добавляет эффект к выбранному клипу" "effect_library.group.builtin": "Встроенные" +"effect_library.group.color": "Color" +"effect_library.group.filter": "Filter" +"effect_library.group.distort": "Distort" +"effect_library.group.keying": "Keying" +"effect_library.group.generator": "Generator" +"effect_library.group.math": "Math" +"effect_library.group.time": "Time" +"effect_library.group.channel": "Channel" +"effect_library.group.transition": "Transition" +"effect_library.group.general": "General" +# Built-in effect (node) and effect-parameter (input) names. The effect +# library and the inspector build these keys at runtime as +# `node..name` and `node..input.`. +"node.polygon.name": "Polygon" +"node.polygon.input.base_in": "Base" +"node.polygon.input.points_in": "Points" +"node.polygon.input.color_in": "Color" +"node.ortho.name": "Orthographic Matrix" +"node.ortho.input.pos_in": "Position" +"node.ortho.input.rot_in": "Rotation" +"node.ortho.input.scale_in": "Scale" +"node.ortho.input.uniform_scale_in": "Uniform Scale" +"node.ortho.input.anchor_in": "Anchor Point" +"node.transform.name": "Transform" +"node.transform.input.tex_in": "Texture" +"node.transform.input.parent_in": "Parent" +"node.transform.input.autoscale_in": "Auto-Scale" +"node.transform.input.interpolation_in": "Interpolation" +"node.transform.input.pos_in": "Position" +"node.transform.input.rot_in": "Rotation" +"node.transform.input.scale_in": "Scale" +"node.transform.input.uniform_scale_in": "Uniform Scale" +"node.transform.input.anchor_in": "Anchor Point" +"node.volume.name": "Volume" +"node.volume.input.samples_in": "Samples" +"node.volume.input.volume_in": "Volume" +"node.pan.name": "Pan" +"node.pan.input.samples_in": "Samples" +"node.pan.input.panning_in": "Pan" +"node.math.name": "Math" +"node.math.input.method_in": "Method" +"node.math.input.param_a_in": "Value" +"node.math.input.param_b_in": "Value" +"node.time.name": "Time" +"node.time.input.enabled_in": "Enabled" +"node.trigonometry.name": "Trigonometry" +"node.trigonometry.input.method_in": "Method" +"node.trigonometry.input.x_in": "Value" +"node.blur.name": "Blur" +"node.blur.input.tex_in": "Input" +"node.blur.input.method_in": "Method" +"node.blur.input.radius_in": "Radius" +"node.blur.input.horiz_in": "Horizontal" +"node.blur.input.vert_in": "Vertical" +"node.blur.input.directional_degrees_in": "Direction" +"node.blur.input.radial_center_in": "Center" +"node.blur.input.repeat_edge_pixels_in": "Repeat Edge Pixels" +"node.solidgenerator.name": "Solid" +"node.solidgenerator.input.color_in": "Color" +"node.merge.name": "Merge" +"node.merge.input.base_in": "Base" +"node.merge.input.blend_in": "Blend" +"node.stroke.name": "Stroke" +"node.stroke.input.tex_in": "Input" +"node.stroke.input.color_in": "Color" +"node.stroke.input.radius_in": "Radius" +"node.stroke.input.opacity_in": "Opacity" +"node.stroke.input.inner_in": "Inner" +"node.textgenerator.name": "Text (Legacy)" +"node.textgenerator.input.text_in": "Text" +"node.textgenerator.input.html_in": "Enable HTML" +"node.textgenerator.input.color_in": "Color" +"node.textgenerator.input.valign_in": "Vertical Align" +"node.textgenerator.input.font_in": "Font" +"node.textgenerator.input.font_size_in": "Font Size" +"node.text2.name": "Text (Legacy)" +"node.text2.input.base_in": "Base" +"node.text2.input.pos_in": "Position" +"node.text2.input.size_in": "Size" +"node.text2.input.color_in": "Color" +"node.text2.input.text_in": "Text" +"node.text2.input.html_in": "Enable HTML" +"node.text2.input.valign_in": "Vertical Align" +"node.text2.input.font_in": "Font" +"node.text2.input.font_size_in": "Font Size" +"node.text3.name": "Text" +"node.text3.input.base_in": "Base" +"node.text3.input.pos_in": "Position" +"node.text3.input.size_in": "Size" +"node.text3.input.plain_text_in": "Text" +"node.text3.input.text_in": "Text" +"node.text3.input.font_family_in": "Font Family" +"node.text3.input.font_size_in": "Font Size" +"node.text3.input.outline_enabled_in": "Outline" +"node.text3.input.outline_color_in": "Outline Color" +"node.text3.input.outline_width_in": "Outline Width" +"node.text3.input.glow_enabled_in": "Glow" +"node.text3.input.glow_color_in": "Glow Color" +"node.text3.input.glow_radius_in": "Glow Radius" +"node.text3.input.color_in": "Color" +"node.text3.input.valign_in": "Vertical Alignment" +"node.text3.input.args_in": "Arguments" +"node.mosaicfilter.name": "Mosaic" +"node.mosaicfilter.input.tex_in": "Texture" +"node.mosaicfilter.input.horiz_in": "Horizontal" +"node.mosaicfilter.input.vert_in": "Vertical" +"node.crop.name": "Crop" +"node.crop.input.tex_in": "Texture" +"node.crop.input.left_in": "Left" +"node.crop.input.top_in": "Top" +"node.crop.input.right_in": "Right" +"node.crop.input.bottom_in": "Bottom" +"node.crop.input.feather_in": "Feather" +"node.value.name": "Value" +"node.value.input.type_in": "Type" +"node.value.input.value_in": "Value" +"node.timeremap.name": "Time Remap" +"node.timeremap.input.time_in": "Time" +"node.timeremap.input.input_in": "Input" +"node.shape.name": "Shape" +"node.shape.input.type_in": "Type" +"node.shape.input.base_in": "Base" +"node.shape.input.pos_in": "Position" +"node.shape.input.size_in": "Size" +"node.shape.input.color_in": "Color" +"node.shape.input.radius_in": "Radius" +"node.colordifferencekey.name": "Color Difference Key" +"node.colordifferencekey.input.tex_in": "Input" +"node.colordifferencekey.input.garbage_in": "Garbage Matte" +"node.colordifferencekey.input.core_in": "Core Matte" +"node.colordifferencekey.input.color_in": "Key Color" +"node.colordifferencekey.input.highlights_in": "Highlights" +"node.colordifferencekey.input.shadows_in": "Shadows" +"node.colordifferencekey.input.mask_only_in": "Show Mask Only" +"node.despill.name": "Despill" +"node.despill.input.tex_in": "Input" +"node.despill.input.color_in": "Key Color" +"node.despill.input.method_in": "Method" +"node.despill.input.preserve_luminance_input": "Preserve Luminance" +"node.group.name": "Group" +"node.opacity.name": "Opacity" +"node.opacity.input.tex_in": "Texture" +"node.opacity.input.opacity_in": "Opacity" +"node.flip.name": "Flip" +"node.flip.input.tex_in": "Input" +"node.flip.input.horiz_in": "Horizontal" +"node.flip.input.vert_in": "Vertical" +"node.noise.name": "Noise" +"node.noise.input.base_in": "Base" +"node.noise.input.strength_in": "Strength" +"node.noise.input.color_in": "Color" +"node.timeoffset.name": "Time Offset" +"node.timeoffset.input.time_in": "Time" +"node.timeoffset.input.input_in": "Input" +"node.transition.name": "Transition" +"node.transition.input.tex_in": "From" +"node.transition.input.blend_in": "To" +"node.transition.input.progress_in": "Progress" +"node.transition.input.type_in": "Type" +"node.transitionfx.name": "Transition FX" +"node.transitionfx.input.tex_in": "From" +"node.transitionfx.input.blend_in": "To" +"node.transitionfx.input.progress_in": "Progress" +"node.transitionfx.input.color_in": "Color" +"node.transitionfx.input.direction_in": "Direction" +"node.transitionfx.input.type_in": "Type" +"node.cornerpin.name": "Corner Pin" +"node.cornerpin.input.tex_in": "Texture" +"node.cornerpin.input.perspective_in": "Perspective" +"node.cornerpin.input.top_left_in": "Top Left" +"node.cornerpin.input.top_right_in": "Top Right" +"node.cornerpin.input.bottom_right_in": "Bottom Right" +"node.cornerpin.input.bottom_left_in": "Bottom Left" +"node.displaytransform.name": "Display Transform" +"node.displaytransform.input.tex_in": "Input" +"node.displaytransform.input.display_in": "Display" +"node.displaytransform.input.view_in": "View" +"node.displaytransform.input.dir_in": "Direction" +"node.ociogradingtransformlinear.name": "OCIO Color Grading (Linear)" +"node.ociogradingtransformlinear.input.tex_in": "Input" +"node.ociogradingtransformlinear.input.ocio_grading_primary_contrast": "Contrast" +"node.ociogradingtransformlinear.input.ocio_grading_primary_offset": "Offset" +"node.ociogradingtransformlinear.input.ocio_grading_primary_exposure": "Exposure" +"node.ociogradingtransformlinear.input.ocio_grading_primary_saturation": "Saturation" +"node.ociogradingtransformlinear.input.ocio_grading_primary_pivot": "Pivot" +"node.ociogradingtransformlinear.input.clamp_black_enable_in": "Enable Black Clamp" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampBlack": "Black Clamp" +"node.ociogradingtransformlinear.input.clamp_white_enable_in": "Enable White Clamp" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampWhite": "White Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.name": "OCIO Color Grading (Log)" +"node.OCIO_NAMESPACEgradingtransformlog.input.tex_in": "Input" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_brightness": "Lift" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_contrast": "Gain" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_gamma": "Gamma" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_saturation": "Saturation" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_pivot": "Pivot" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_black_enable_in": "Enable Black Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampBlack": "Black Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_white_enable_in": "Enable White Clamp" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampWhite": "White Clamp" +"node.whitebalance.name": "White Balance" +"node.whitebalance.input.tex_in": "Input" +"node.whitebalance.input.temperature_in": "Temperature (K)" +"node.whitebalance.input.tint_in": "Tint" +"node.ociolut.name": "OCIO LUT" +"node.ociolut.input.tex_in": "Input" +"node.ociolut.input.lut_file_in": "LUT File" +"node.ociolut.input.lut_dir_in": "Direction" +"node.threewaycolor.name": "Three-Way Color" +"node.threewaycolor.input.tex_in": "Input" +"node.threewaycolor.input.shadows_color_in": "Shadows" +"node.threewaycolor.input.midtones_color_in": "Midtones" +"node.threewaycolor.input.highlights_color_in": "Highlights" +"node.threewaycolor.input.shadows_amount_in": "Shadows Amount" +"node.threewaycolor.input.midtones_amount_in": "Midtones Amount" +"node.threewaycolor.input.highlights_amount_in": "Highlights Amount" +"node.chromakey.name": "Chroma Key" +"node.chromakey.input.tex_in": "Input" +"node.chromakey.input.color_key": "Key Color" +"node.chromakey.input.lower_tolerance_in": "Lower Tolerance" +"node.chromakey.input.upper_tolerance_in": "Upper Tolerance" +"node.chromakey.input.garbage_in": "Garbage Matte" +"node.chromakey.input.core_in": "Core Matte" +"node.chromakey.input.highlights_in": "Highlights" +"node.chromakey.input.shadows_in": "Shadows" +"node.chromakey.input.invert_in": "Invert Mask" +"node.chromakey.input.mask_only_in": "Show Mask Only" +"node.mask.name": "Mask" +"node.mask.input.base_in": "Texture" +"node.mask.input.points_in": "Points" +"node.mask.input.color_in": "Color" +"node.mask.input.invert_in": "Invert" +"node.mask.input.feather_in": "Feather" +"node.dropshadow.name": "Drop Shadow" +"node.dropshadow.input.tex_in": "Texture" +"node.dropshadow.input.color_in": "Color" +"node.dropshadow.input.distance_in": "Distance" +"node.dropshadow.input.angle_in": "Angle" +"node.dropshadow.input.radius_in": "Softness" +"node.dropshadow.input.opacity_in": "Opacity" +"node.dropshadow.input.fast_in": "Faster (Lower Quality)" +"node.timeformat.name": "Time Format" +"node.timeformat.input.time_in": "Time" +"node.timeformat.input.format_in": "Format" +"node.timeformat.input.localtime_in": "Interpret time as local time" +"node.wave.name": "Wave" +"node.wave.input.tex_in": "Input" +"node.wave.input.frequency_in": "Frequency" +"node.wave.input.intensity_in": "Intensity" +"node.wave.input.evolution_in": "Evolution" +"node.wave.input.vertical_in": "Direction" +"node.tile.name": "Tile" +"node.tile.input.tex_in": "Input" +"node.tile.input.scale_in": "Scale" +"node.tile.input.position_in": "Position" +"node.tile.input.anchor_in": "Anchor" +"node.tile.input.mirrorx_in": "Mirror Horizontally" +"node.tile.input.mirrory_in": "Mirror Vertically" +"node.swirl.name": "Swirl" +"node.swirl.input.tex_in": "Input" +"node.swirl.input.radius_in": "Radius" +"node.swirl.input.angle_in": "Angle" +"node.swirl.input.pos_in": "Position" +"node.ripple.name": "Ripple" +"node.ripple.input.tex_in": "Input" +"node.ripple.input.evolution_in": "Evolution" +"node.ripple.input.intensity_in": "Intensity" +"node.ripple.input.frequency_in": "Frequency" +"node.ripple.input.position_in": "Position" +"node.ripple.input.stretch_in": "Stretch" +"node.colormatrix.name": "Color Matrix" +"node.colormatrix.input.tex_in": "Input" +"node.colormatrix.input.m0": "R <- R" +"node.colormatrix.input.m1": "R <- G" +"node.colormatrix.input.m2": "R <- B" +"node.colormatrix.input.m3": "R <- A" +"node.colormatrix.input.m4": "G <- R" +"node.colormatrix.input.m5": "G <- G" +"node.colormatrix.input.m6": "G <- B" +"node.colormatrix.input.m7": "G <- A" +"node.colormatrix.input.m8": "B <- R" +"node.colormatrix.input.m9": "B <- G" +"node.colormatrix.input.m10": "B <- B" +"node.colormatrix.input.m11": "B <- A" +"node.colormatrix.input.m12": "A <- R" +"node.colormatrix.input.m13": "A <- G" +"node.colormatrix.input.m14": "A <- B" +"node.colormatrix.input.m15": "A <- A" +"node.dilate.name": "Dilate" +"node.dilate.input.tex_in": "Input" +"node.dilate.input.radius_in": "Radius" +"node.edgedetect.name": "Edge Detect" +"node.edgedetect.input.tex_in": "Input" +"node.edgedetect.input.threshold_in": "Threshold" +"node.erode.name": "Erode" +"node.erode.input.tex_in": "Input" +"node.erode.input.radius_in": "Radius" +"node.colorcorrect.name": "Color Correct" +"node.colorcorrect.input.tex_in": "Input" +"node.colorcorrect.input.saturation_in": "Saturation" +"node.colorcorrect.input.contrast_in": "Contrast" +"node.colorcorrect.input.gamma_in": "Gamma" +"node.colorcorrect.input.gain_in": "Gain" +"node.colorcorrect.input.offset_in": "Offset" +"node.gamma.name": "Gamma" +"node.gamma.input.tex_in": "Input" +"node.gamma.input.gamma_in": "Gamma" +"node.saturation.name": "Saturation" +"node.saturation.input.tex_in": "Input" +"node.saturation.input.saturation_in": "Saturation" +"node.invert.name": "Invert" +"node.invert.input.tex_in": "Input" +"node.invert.input.invert_r_in": "Invert Red" +"node.invert.input.invert_g_in": "Invert Green" +"node.invert.input.invert_b_in": "Invert Blue" +"node.invert.input.invert_a_in": "Invert Alpha" +"node.clamp.name": "Clamp" +"node.clamp.input.tex_in": "Input" +"node.clamp.input.min_in": "Min" +"node.clamp.input.max_in": "Max" +"node.grade.name": "Grade" +"node.grade.input.tex_in": "Input" +"node.grade.input.blackpoint_in": "Black Point" +"node.grade.input.whitepoint_in": "White Point" +"node.grade.input.black_in": "Black Output" +"node.grade.input.white_in": "White Output" +"node.grade.input.gamma_in": "Gamma" +"node.dirblur.name": "Directional Blur" +"node.dirblur.input.tex_in": "Input" +"node.dirblur.input.amount_in": "Amount" +"node.dirblur.input.angle_in": "Angle" +"node.sharpen.name": "Sharpen" +"node.sharpen.input.tex_in": "Input" +"node.sharpen.input.amount_in": "Amount" +"node.dissolve.name": "Dissolve" +"node.dissolve.input.tex_in": "Input" +"node.dissolve.input.blend_in": "Blend" +"node.dissolve.input.mix_in": "Mix" +"node.keymix.name": "KeyMix" +"node.keymix.input.tex_in": "Input" +"node.keymix.input.blend_in": "Blend" +"node.keymix.input.mask_in": "Mask" +"node.premult.name": "Premultiply" +"node.premult.input.tex_in": "Input" +"node.premult.input.premult_channel_in": "Channel" +"node.unpremult.name": "Unpremultiply" +"node.unpremult.input.tex_in": "Input" +"node.unpremult.input.unpremult_channel_in": "Channel" +"node.position.name": "Position" +"node.position.input.tex_in": "Input" +"node.position.input.offset_in": "Translate" +"node.mirror.name": "Mirror" +"node.mirror.input.tex_in": "Input" +"node.mirror.input.horizontal_in": "Horizontal (flop)" +"node.mirror.input.vertical_in": "Vertical (flip)" +"node.checkerboard.name": "Checkerboard" +"node.checkerboard.input.base_in": "Base" +"node.checkerboard.input.size_in": "Box Size" +"node.checkerboard.input.color1_in": "Color 1" +"node.checkerboard.input.color2_in": "Color 2" +"node.colorbars.name": "Color Bars" +"node.colorbars.input.base_in": "Base" +"node.colorbars.input.standard_in": "Standard" +"node.ramp.name": "Ramp" +"node.ramp.input.base_in": "Base" +"node.ramp.input.point0_in": "Point 0" +"node.ramp.input.point1_in": "Point 1" +"node.ramp.input.color0_in": "Color 0" +"node.ramp.input.color1_in": "Color 1" +"node.multicam.name": "Multi-Cam" +"node.multicam.input.current_in": "Current" +"node.multicam.input.sources_in": "Sources" +"node.multicam.input.sequence_in": "Sequence" +"node.multicam.input.sequence_type_in": "Sequence Type" "status.ready": "Готово" "status.cache": "Кэш: включён" "status.proxy.on": "Прокси: вкл" @@ -481,6 +855,8 @@ "node.category.group": "Группа" "project.context.new": "Создать" "project.new_sequence": "Новая последовательность" +"project.add_text_footage": "Add Text Footage" +"timeline.context.add_adjustment_layer": "Add Adjustment Layer" "project.context.reveal_in_finder": "Показать в Finder" "project.context.replace_footage": "Заменить медиафайлы" "project.context.rename": "Переименовать" diff --git a/assets/i18n/zh-CN.yaml b/assets/i18n/zh-CN.yaml index bcd82e187..18438d02a 100644 --- a/assets/i18n/zh-CN.yaml +++ b/assets/i18n/zh-CN.yaml @@ -169,6 +169,380 @@ "multicam.switch_9": "切换到机位 9" "effect_library.hint": "双击添加到选中片段" "effect_library.group.builtin": "内置" +"effect_library.group.color": "调色" +"effect_library.group.filter": "滤镜" +"effect_library.group.distort": "扭曲" +"effect_library.group.keying": "键控" +"effect_library.group.generator": "生成器" +"effect_library.group.math": "数学" +"effect_library.group.time": "时间" +"effect_library.group.channel": "通道" +"effect_library.group.transition": "转场" +"effect_library.group.general": "通用" +# Built-in effect (node) and effect-parameter (input) names. The effect +# library and the inspector build these keys at runtime as +# `node..name` and `node..input.`. +"node.polygon.name": "多边形" +"node.polygon.input.base_in": "基础" +"node.polygon.input.points_in": "顶点" +"node.polygon.input.color_in": "颜色" +"node.ortho.name": "正交矩阵" +"node.ortho.input.pos_in": "位置" +"node.ortho.input.rot_in": "旋转" +"node.ortho.input.scale_in": "缩放" +"node.ortho.input.uniform_scale_in": "等比缩放" +"node.ortho.input.anchor_in": "锚点" +"node.transform.name": "变换" +"node.transform.input.tex_in": "纹理" +"node.transform.input.parent_in": "父级" +"node.transform.input.autoscale_in": "自动缩放" +"node.transform.input.interpolation_in": "插值" +"node.transform.input.pos_in": "位置" +"node.transform.input.rot_in": "旋转" +"node.transform.input.scale_in": "缩放" +"node.transform.input.uniform_scale_in": "等比缩放" +"node.transform.input.anchor_in": "锚点" +"node.volume.name": "音量" +"node.volume.input.samples_in": "采样数" +"node.volume.input.volume_in": "音量" +"node.pan.name": "声像" +"node.pan.input.samples_in": "采样数" +"node.pan.input.panning_in": "声像" +"node.math.name": "数学" +"node.math.input.method_in": "方法" +"node.math.input.param_a_in": "数值" +"node.math.input.param_b_in": "数值" +"node.time.name": "时间" +"node.time.input.enabled_in": "启用" +"node.trigonometry.name": "三角函数" +"node.trigonometry.input.method_in": "方法" +"node.trigonometry.input.x_in": "数值" +"node.blur.name": "模糊" +"node.blur.input.tex_in": "输入" +"node.blur.input.method_in": "方法" +"node.blur.input.radius_in": "半径" +"node.blur.input.horiz_in": "水平" +"node.blur.input.vert_in": "垂直" +"node.blur.input.directional_degrees_in": "方向" +"node.blur.input.radial_center_in": "中心" +"node.blur.input.repeat_edge_pixels_in": "重复边缘像素" +"node.solidgenerator.name": "纯色" +"node.solidgenerator.input.color_in": "颜色" +"node.merge.name": "合并" +"node.merge.input.base_in": "基础" +"node.merge.input.blend_in": "混合层" +"node.stroke.name": "描边" +"node.stroke.input.tex_in": "输入" +"node.stroke.input.color_in": "颜色" +"node.stroke.input.radius_in": "半径" +"node.stroke.input.opacity_in": "不透明度" +"node.stroke.input.inner_in": "内部" +"node.textgenerator.name": "文本(旧版)" +"node.textgenerator.input.text_in": "文本" +"node.textgenerator.input.html_in": "启用 HTML" +"node.textgenerator.input.color_in": "颜色" +"node.textgenerator.input.valign_in": "垂直对齐" +"node.textgenerator.input.font_in": "字体" +"node.textgenerator.input.font_size_in": "字号" +"node.text2.name": "文本(旧版)" +"node.text2.input.base_in": "基础" +"node.text2.input.pos_in": "位置" +"node.text2.input.size_in": "尺寸" +"node.text2.input.color_in": "颜色" +"node.text2.input.text_in": "文本" +"node.text2.input.html_in": "启用 HTML" +"node.text2.input.valign_in": "垂直对齐" +"node.text2.input.font_in": "字体" +"node.text2.input.font_size_in": "字号" +"node.text3.name": "文本" +"node.text3.input.base_in": "基础" +"node.text3.input.pos_in": "位置" +"node.text3.input.size_in": "尺寸" +"node.text3.input.plain_text_in": "文本" +"node.text3.input.text_in": "文本" +"node.text3.input.font_family_in": "字体族" +"node.text3.input.font_size_in": "字号" +"node.text3.input.outline_enabled_in": "描边" +"node.text3.input.outline_color_in": "描边颜色" +"node.text3.input.outline_width_in": "描边宽度" +"node.text3.input.glow_enabled_in": "辉光" +"node.text3.input.glow_color_in": "辉光颜色" +"node.text3.input.glow_radius_in": "辉光半径" +"node.text3.input.color_in": "颜色" +"node.text3.input.valign_in": "垂直对齐" +"node.text3.input.args_in": "参数列表" +"node.mosaicfilter.name": "马赛克" +"node.mosaicfilter.input.tex_in": "纹理" +"node.mosaicfilter.input.horiz_in": "水平" +"node.mosaicfilter.input.vert_in": "垂直" +"node.crop.name": "裁剪" +"node.crop.input.tex_in": "纹理" +"node.crop.input.left_in": "左" +"node.crop.input.top_in": "上" +"node.crop.input.right_in": "右" +"node.crop.input.bottom_in": "下" +"node.crop.input.feather_in": "羽化" +"node.value.name": "数值" +"node.value.input.type_in": "类型" +"node.value.input.value_in": "数值" +"node.timeremap.name": "时间重映射" +"node.timeremap.input.time_in": "时间" +"node.timeremap.input.input_in": "输入" +"node.shape.name": "形状" +"node.shape.input.type_in": "类型" +"node.shape.input.base_in": "基础" +"node.shape.input.pos_in": "位置" +"node.shape.input.size_in": "尺寸" +"node.shape.input.color_in": "颜色" +"node.shape.input.radius_in": "半径" +"node.colordifferencekey.name": "色差键控" +"node.colordifferencekey.input.tex_in": "输入" +"node.colordifferencekey.input.garbage_in": "垃圾遮罩" +"node.colordifferencekey.input.core_in": "核心遮罩" +"node.colordifferencekey.input.color_in": "键控色" +"node.colordifferencekey.input.highlights_in": "高光" +"node.colordifferencekey.input.shadows_in": "阴影" +"node.colordifferencekey.input.mask_only_in": "仅显示遮罩" +"node.despill.name": "去溢色" +"node.despill.input.tex_in": "输入" +"node.despill.input.color_in": "键控色" +"node.despill.input.method_in": "方法" +"node.despill.input.preserve_luminance_input": "保持亮度" +"node.group.name": "组" +"node.opacity.name": "不透明度" +"node.opacity.input.tex_in": "纹理" +"node.opacity.input.opacity_in": "不透明度" +"node.flip.name": "翻转" +"node.flip.input.tex_in": "输入" +"node.flip.input.horiz_in": "水平" +"node.flip.input.vert_in": "垂直" +"node.noise.name": "噪波" +"node.noise.input.base_in": "基础" +"node.noise.input.strength_in": "强度" +"node.noise.input.color_in": "颜色" +"node.timeoffset.name": "时间偏移" +"node.timeoffset.input.time_in": "时间" +"node.timeoffset.input.input_in": "输入" +"node.transition.name": "转场" +"node.transition.input.tex_in": "起点" +"node.transition.input.blend_in": "终点" +"node.transition.input.progress_in": "进度" +"node.transition.input.type_in": "类型" +"node.transitionfx.name": "转场特效" +"node.transitionfx.input.tex_in": "起点" +"node.transitionfx.input.blend_in": "终点" +"node.transitionfx.input.progress_in": "进度" +"node.transitionfx.input.color_in": "颜色" +"node.transitionfx.input.direction_in": "方向" +"node.transitionfx.input.type_in": "类型" +"node.cornerpin.name": "角点定位" +"node.cornerpin.input.tex_in": "纹理" +"node.cornerpin.input.perspective_in": "透视" +"node.cornerpin.input.top_left_in": "左上角" +"node.cornerpin.input.top_right_in": "右上角" +"node.cornerpin.input.bottom_right_in": "右下角" +"node.cornerpin.input.bottom_left_in": "左下角" +"node.displaytransform.name": "显示变换" +"node.displaytransform.input.tex_in": "输入" +"node.displaytransform.input.display_in": "显示" +"node.displaytransform.input.view_in": "视图" +"node.displaytransform.input.dir_in": "方向" +"node.ociogradingtransformlinear.name": "OCIO 调色(线性)" +"node.ociogradingtransformlinear.input.tex_in": "输入" +"node.ociogradingtransformlinear.input.ocio_grading_primary_contrast": "对比度" +"node.ociogradingtransformlinear.input.ocio_grading_primary_offset": "偏移" +"node.ociogradingtransformlinear.input.ocio_grading_primary_exposure": "曝光" +"node.ociogradingtransformlinear.input.ocio_grading_primary_saturation": "饱和度" +"node.ociogradingtransformlinear.input.ocio_grading_primary_pivot": "轴心" +"node.ociogradingtransformlinear.input.clamp_black_enable_in": "启用黑场限幅" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampBlack": "黑场限幅" +"node.ociogradingtransformlinear.input.clamp_white_enable_in": "启用白场限幅" +"node.ociogradingtransformlinear.input.ocio_grading_primary_clampWhite": "白场限幅" +"node.OCIO_NAMESPACEgradingtransformlog.name": "OCIO 调色(对数)" +"node.OCIO_NAMESPACEgradingtransformlog.input.tex_in": "输入" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_brightness": "提升" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_contrast": "增益" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_gamma": "伽马" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_saturation": "饱和度" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_pivot": "轴心" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_black_enable_in": "启用黑场限幅" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampBlack": "黑场限幅" +"node.OCIO_NAMESPACEgradingtransformlog.input.clamp_white_enable_in": "启用白场限幅" +"node.OCIO_NAMESPACEgradingtransformlog.input.OCIO_NAMESPACE_grading_primary_clampWhite": "白场限幅" +"node.whitebalance.name": "白平衡" +"node.whitebalance.input.tex_in": "输入" +"node.whitebalance.input.temperature_in": "色温 (K)" +"node.whitebalance.input.tint_in": "色调" +"node.ociolut.name": "OCIO 查找表" +"node.ociolut.input.tex_in": "输入" +"node.ociolut.input.lut_file_in": "LUT 文件" +"node.ociolut.input.lut_dir_in": "方向" +"node.threewaycolor.name": "三向调色" +"node.threewaycolor.input.tex_in": "输入" +"node.threewaycolor.input.shadows_color_in": "阴影" +"node.threewaycolor.input.midtones_color_in": "中间调" +"node.threewaycolor.input.highlights_color_in": "高光" +"node.threewaycolor.input.shadows_amount_in": "阴影量" +"node.threewaycolor.input.midtones_amount_in": "中间调量" +"node.threewaycolor.input.highlights_amount_in": "高光量" +"node.chromakey.name": "色度键" +"node.chromakey.input.tex_in": "输入" +"node.chromakey.input.color_key": "键控色" +"node.chromakey.input.lower_tolerance_in": "下限容差" +"node.chromakey.input.upper_tolerance_in": "上限容差" +"node.chromakey.input.garbage_in": "垃圾遮罩" +"node.chromakey.input.core_in": "核心遮罩" +"node.chromakey.input.highlights_in": "高光" +"node.chromakey.input.shadows_in": "阴影" +"node.chromakey.input.invert_in": "反转遮罩" +"node.chromakey.input.mask_only_in": "仅显示遮罩" +"node.mask.name": "遮罩" +"node.mask.input.base_in": "纹理" +"node.mask.input.points_in": "顶点" +"node.mask.input.color_in": "颜色" +"node.mask.input.invert_in": "反转" +"node.mask.input.feather_in": "羽化" +"node.dropshadow.name": "投影" +"node.dropshadow.input.tex_in": "纹理" +"node.dropshadow.input.color_in": "颜色" +"node.dropshadow.input.distance_in": "距离" +"node.dropshadow.input.angle_in": "角度" +"node.dropshadow.input.radius_in": "柔和度" +"node.dropshadow.input.opacity_in": "不透明度" +"node.dropshadow.input.fast_in": "更快(较低质量)" +"node.timeformat.name": "时间格式" +"node.timeformat.input.time_in": "时间" +"node.timeformat.input.format_in": "格式" +"node.timeformat.input.localtime_in": "按本地时间解释" +"node.wave.name": "波纹" +"node.wave.input.tex_in": "输入" +"node.wave.input.frequency_in": "频率" +"node.wave.input.intensity_in": "强度" +"node.wave.input.evolution_in": "演化" +"node.wave.input.vertical_in": "方向" +"node.tile.name": "平铺" +"node.tile.input.tex_in": "输入" +"node.tile.input.scale_in": "缩放" +"node.tile.input.position_in": "位置" +"node.tile.input.anchor_in": "锚点" +"node.tile.input.mirrorx_in": "水平镜像" +"node.tile.input.mirrory_in": "垂直镜像" +"node.swirl.name": "漩涡" +"node.swirl.input.tex_in": "输入" +"node.swirl.input.radius_in": "半径" +"node.swirl.input.angle_in": "角度" +"node.swirl.input.pos_in": "位置" +"node.ripple.name": "涟漪" +"node.ripple.input.tex_in": "输入" +"node.ripple.input.evolution_in": "演化" +"node.ripple.input.intensity_in": "强度" +"node.ripple.input.frequency_in": "频率" +"node.ripple.input.position_in": "位置" +"node.ripple.input.stretch_in": "拉伸" +"node.colormatrix.name": "颜色矩阵" +"node.colormatrix.input.tex_in": "输入" +"node.colormatrix.input.m0": "R ← R" +"node.colormatrix.input.m1": "R ← G" +"node.colormatrix.input.m2": "R ← B" +"node.colormatrix.input.m3": "R ← A" +"node.colormatrix.input.m4": "G ← R" +"node.colormatrix.input.m5": "G ← G" +"node.colormatrix.input.m6": "G ← B" +"node.colormatrix.input.m7": "G ← A" +"node.colormatrix.input.m8": "B ← R" +"node.colormatrix.input.m9": "B ← G" +"node.colormatrix.input.m10": "B ← B" +"node.colormatrix.input.m11": "B ← A" +"node.colormatrix.input.m12": "A ← R" +"node.colormatrix.input.m13": "A ← G" +"node.colormatrix.input.m14": "A ← B" +"node.colormatrix.input.m15": "A ← A" +"node.dilate.name": "膨胀" +"node.dilate.input.tex_in": "输入" +"node.dilate.input.radius_in": "半径" +"node.edgedetect.name": "边缘检测" +"node.edgedetect.input.tex_in": "输入" +"node.edgedetect.input.threshold_in": "阈值" +"node.erode.name": "腐蚀" +"node.erode.input.tex_in": "输入" +"node.erode.input.radius_in": "半径" +"node.colorcorrect.name": "颜色校正" +"node.colorcorrect.input.tex_in": "输入" +"node.colorcorrect.input.saturation_in": "饱和度" +"node.colorcorrect.input.contrast_in": "对比度" +"node.colorcorrect.input.gamma_in": "伽马" +"node.colorcorrect.input.gain_in": "增益" +"node.colorcorrect.input.offset_in": "偏移" +"node.gamma.name": "伽马" +"node.gamma.input.tex_in": "输入" +"node.gamma.input.gamma_in": "伽马" +"node.saturation.name": "饱和度" +"node.saturation.input.tex_in": "输入" +"node.saturation.input.saturation_in": "饱和度" +"node.invert.name": "反转" +"node.invert.input.tex_in": "输入" +"node.invert.input.invert_r_in": "反转红" +"node.invert.input.invert_g_in": "反转绿" +"node.invert.input.invert_b_in": "反转蓝" +"node.invert.input.invert_a_in": "反转 Alpha" +"node.clamp.name": "限幅" +"node.clamp.input.tex_in": "输入" +"node.clamp.input.min_in": "最小" +"node.clamp.input.max_in": "最大" +"node.grade.name": "调色" +"node.grade.input.tex_in": "输入" +"node.grade.input.blackpoint_in": "黑点" +"node.grade.input.whitepoint_in": "白点" +"node.grade.input.black_in": "黑场输出" +"node.grade.input.white_in": "白场输出" +"node.grade.input.gamma_in": "伽马" +"node.dirblur.name": "方向模糊" +"node.dirblur.input.tex_in": "输入" +"node.dirblur.input.amount_in": "强度" +"node.dirblur.input.angle_in": "角度" +"node.sharpen.name": "锐化" +"node.sharpen.input.tex_in": "输入" +"node.sharpen.input.amount_in": "强度" +"node.dissolve.name": "溶解" +"node.dissolve.input.tex_in": "输入" +"node.dissolve.input.blend_in": "混合层" +"node.dissolve.input.mix_in": "混合" +"node.keymix.name": "键控混合" +"node.keymix.input.tex_in": "输入" +"node.keymix.input.blend_in": "混合层" +"node.keymix.input.mask_in": "遮罩" +"node.premult.name": "预乘" +"node.premult.input.tex_in": "输入" +"node.premult.input.premult_channel_in": "通道" +"node.unpremult.name": "反预乘" +"node.unpremult.input.tex_in": "输入" +"node.unpremult.input.unpremult_channel_in": "通道" +"node.position.name": "位置" +"node.position.input.tex_in": "输入" +"node.position.input.offset_in": "平移" +"node.mirror.name": "镜像" +"node.mirror.input.tex_in": "输入" +"node.mirror.input.horizontal_in": "水平(翻转)" +"node.mirror.input.vertical_in": "垂直(翻转)" +"node.checkerboard.name": "棋盘格" +"node.checkerboard.input.base_in": "基础" +"node.checkerboard.input.size_in": "盒子尺寸" +"node.checkerboard.input.color1_in": "颜色 1" +"node.checkerboard.input.color2_in": "颜色 2" +"node.colorbars.name": "彩条" +"node.colorbars.input.base_in": "基础" +"node.colorbars.input.standard_in": "标准" +"node.ramp.name": "渐变" +"node.ramp.input.base_in": "基础" +"node.ramp.input.point0_in": "点 0" +"node.ramp.input.point1_in": "点 1" +"node.ramp.input.color0_in": "颜色 0" +"node.ramp.input.color1_in": "颜色 1" +"node.multicam.name": "多机位" +"node.multicam.input.current_in": "当前" +"node.multicam.input.sources_in": "源" +"node.multicam.input.sequence_in": "序列" +"node.multicam.input.sequence_type_in": "序列类型" "status.ready": "就绪" "status.cache": "缓存:已启用" "status.proxy.on": "代理:开" @@ -479,6 +853,8 @@ "node.category.group": "组" "project.context.new": "新建" "project.new_sequence": "新建序列" +"project.add_text_footage": "添加文本素材" +"timeline.context.add_adjustment_layer": "添加调整图层" "project.context.reveal_in_finder": "在 Finder 中显示" "project.context.replace_footage": "替换素材" "project.context.rename": "重命名" diff --git a/crates/oak-app/src/app.rs b/crates/oak-app/src/app.rs index dd5092189..a86c40d45 100644 --- a/crates/oak-app/src/app.rs +++ b/crates/oak-app/src/app.rs @@ -655,6 +655,23 @@ impl OakApp { ) .detach(); + // The project explorer's 添加文本素材 button creates a text + // generator bin entry in the open project. + cx.subscribe( + &panels.project, + |this, + _panel, + _event: &crate::panels::project_explorer::NewTextFootageRequested, + cx| { + this.engine.update(cx, |engine, cx| { + if let Err(e) = engine.create_text_footage(cx) { + println!("[shell] add text footage: {e}"); + } + }); + }, + ) + .detach(); + // A footage drop onto a timeline with no open sequence goes through // the probe-vs-manual choice before any sequence is created. cx.subscribe( @@ -5448,6 +5465,46 @@ mod tests { assert_eq!(drops[0].time, gpui::timeline::Frame(50)); } + /// The project explorer's 添加文本素材 button creates a text generator + /// bin entry: the click emits `NewTextFootageRequested`, the shell's + /// subscription calls `AppEngine::create_text_footage`, and the bin + /// lists the new entry (labelled 文本, the shared node label) beside + /// the demo rows. + #[gpui::test] + async fn clicking_add_text_footage_creates_a_text_entry(cx: &mut TestAppContext) { + let _guard = crate::i18n::lang_test_lock() + .lock() + .unwrap_or_else(|e| e.into_inner()); + let (window, root) = mock_shell(cx); + let mut vcx = VisualTestContext::from_window(window.into(), cx); + + let button = vcx + .debug_bounds("project-add-text-footage") + .expect("the explorer's 添加文本素材 button is rendered"); + vcx.simulate_click(button.center(), gpui::Modifiers::none()); + drop(vcx); + cx.run_until_parked(); + + let created = cx.read(|app| root.read(app).engine.read(app).text_footages().to_vec()); + assert_eq!( + created, + vec![100], + "the click reached the engine exactly once" + ); + let listed = cx.read(|app| { + use gpui_widgets::project_explorer::ProjectDataSource as _; + root.read(app) + .engine + .read(app) + .roots() + .iter() + .any(|e| { + e.id == 100 && e.name == crate::oakui::graphops::TEXT_FOOTAGE_LABEL + }) + }); + assert!(listed, "the created text entry is listed in the bin"); + } + /// The project explorer lists root-level imported footage in BOTH views: /// the tree shows the entry as a row, and after switching to the icon /// grid the same entry (plus folder children) appears as icons. diff --git a/crates/oak-app/src/i18n.rs b/crates/oak-app/src/i18n.rs index 8a80a3d68..20da1be37 100644 --- a/crates/oak-app/src/i18n.rs +++ b/crates/oak-app/src/i18n.rs @@ -141,6 +141,27 @@ pub fn tr(key: &'static str) -> &'static str { key } +/// Translates a key built at runtime — the built-in effect and parameter +/// names, whose keys are `node..name` and +/// `node..input.` (see +/// [`oakui::effectchain::addable_effects`](crate::oakui::effectchain::addable_effects)). +/// +/// Same lookup as [`tr`] — the active language, then `en-US` — with the +/// node layer's own English name as the last resort: `tr` takes +/// `&'static str` keys and returns the key itself on a miss, which for a +/// formatted key would put a raw `node.…` identifier in the UI. +pub fn tr_or(key: &str, fallback: &str) -> String { + let tables = tables().read().unwrap_or_else(|e| e.into_inner()); + let current = language_code(); + if let Some(value) = tables.get(¤t).and_then(|t| t.get(key)) { + return value.to_string(); + } + if let Some(value) = tables.get(FALLBACK_CODE).and_then(|t| t.get(key)) { + return value.to_string(); + } + fallback.to_string() +} + /// The keys the gpui widget crates localize through /// `gpui_widgets::i18n` (viewer transport labels, effect-stack empty state). pub const WIDGET_KEYS: &[&str] = &[ @@ -358,6 +379,41 @@ mod tests { } } + /// Every built-in node type the factory registers carries a + /// `node..name` entry in the reference pack *and* in + /// the translation: the effect library and the inspector build that key + /// at runtime, so a node without an entry would show its English name + /// in every language — and one whose zh value repeats the English name + /// was never translated at all. The matching parameter keys are held by + /// `effectchain`'s own test against the live parameter list. + #[test] + fn every_builtin_node_name_is_translated() { + let tables = tables().read().unwrap_or_else(|e| e.into_inner()); + let en = tables.get("en-US").expect("en-US pack"); + let zh = tables.get("zh-CN").expect("zh-CN pack"); + let mut nodes = 0usize; + for meta in oak_node::factory::Factory::global().entries() { + let suffix = meta.type_id.rsplit('.').next().unwrap_or(meta.type_id); + let key = format!("node.{suffix}.name"); + let en_value = en + .get(&key) + .unwrap_or_else(|| panic!("{key} missing from the en-US pack")); + assert_eq!( + *en_value, meta.name, + "{key} does not carry the node's English name" + ); + let zh_value = zh + .get(&key) + .unwrap_or_else(|| panic!("{key} missing from the zh-CN pack")); + assert_ne!(zh_value, en_value, "{key} is not translated ({en_value:?})"); + nodes += 1; + } + assert!( + nodes >= 60, + "the factory registers the built-in node types ({nodes})" + ); + } + /// `tr` falls back from the active language to English and then to /// the key itself. #[test] diff --git a/crates/oak-app/src/oakui/effectchain.rs b/crates/oak-app/src/oakui/effectchain.rs index d617b57e0..fb6e4b824 100644 --- a/crates/oak-app/src/oakui/effectchain.rs +++ b/crates/oak-app/src/oakui/effectchain.rs @@ -93,11 +93,96 @@ pub fn is_enabled(g: &Graph, node: NodeId) -> bool { .unwrap_or(false) } +/// The built-in effect group keys, in presentation order. +/// +/// The effect library and the inspector's add menu head the built-in +/// effects with these groups, in this order; `general` collects every +/// category without a group of its own and sorts last. The keys are +/// **language-pack keys**, never display strings: a header's label is +/// [`group_label`]'s `effect_library.group.` lookup. +pub const GROUP_KEYS: &[&str] = &[ + "color", + "filter", + "distort", + "keying", + "generator", + "math", + "time", + "channel", + "transition", + "general", +]; + +/// The group key of a node [`Category`](oak_node::node::Category). +/// +/// The category enum mirrors the C++ `Node::CategoryID`, which is finer +/// grained than the create menu: outputs, plain effects, inputs, OpenFX +/// plugins, timeline and group nodes have no group of their own and land +/// in `general`. `time` / `channel` / `transition` are part of the key +/// set (the language packs carry their labels) but no category maps to +/// them yet. +pub fn category_group_key(category: oak_node::node::Category) -> &'static str { + use oak_node::node::Category; + match category { + Category::Color => "color", + Category::Filter => "filter", + Category::Distort => "distort", + Category::Keying => "keying", + Category::Generator => "generator", + Category::Math => "math", + _ => "general", + } +} + +/// The display label of an effect-group key: the localized +/// `effect_library.group.` string for the built-in keys, and the key +/// itself for OpenFX sub-categories (their labels come from the plugin +/// descriptor, not from the language packs). +pub fn group_label(group_key: &str) -> String { + match group_key { + "color" => crate::i18n::tr("effect_library.group.color"), + "filter" => crate::i18n::tr("effect_library.group.filter"), + "distort" => crate::i18n::tr("effect_library.group.distort"), + "keying" => crate::i18n::tr("effect_library.group.keying"), + "generator" => crate::i18n::tr("effect_library.group.generator"), + "math" => crate::i18n::tr("effect_library.group.math"), + "time" => crate::i18n::tr("effect_library.group.time"), + "channel" => crate::i18n::tr("effect_library.group.channel"), + "transition" => crate::i18n::tr("effect_library.group.transition"), + "general" => crate::i18n::tr("effect_library.group.general"), + // OpenFX sub-categories (and any key a future pack ships without a + // label): shown verbatim rather than as a raw pack key. + other => other, + } + .to_string() +} + +/// The language-pack key of a built-in node's display name, built at +/// runtime: `node..name`, the suffix being the type id's +/// last dotted segment (`org.olivevideoeditor.Olive.colorcorrect` → +/// `colorcorrect`). +fn node_name_key(type_id: &str) -> String { + format!("node.{}.name", type_suffix(type_id)) +} + +/// The language-pack key of a built-in node input's display name: +/// `node..input.`. +fn input_name_key(type_id: &str, input_id: &str) -> String { + format!("node.{}.input.{input_id}", type_suffix(type_id)) +} + +/// The last dotted segment of a node type id — the language-pack key's +/// ``. +fn type_suffix(type_id: &str) -> &str { + type_id.rsplit('.').next().unwrap_or(type_id) +} + /// The effect types the user can add to a clip's chain — the built-in /// factory entries flagged `video_effect` and not hidden from the create -/// menu (no group), plus every runtime-registered OpenFX plugin entry -/// (grouped by its sub-category: Filter / Generator / Transition / -/// General — the C++ `factorymenu.cpp` OpenFX branch). +/// menu (grouped by their first category, see [`category_group_key`]), +/// plus every runtime-registered OpenFX plugin entry (grouped by its +/// sub-category: Filter / Generator / Transition / General — the C++ +/// `factorymenu.cpp` OpenFX branch). pub fn addable_effects() -> Vec { use super::engine::EffectEntry; use oak_node::node::flags as node_flags; @@ -115,10 +200,18 @@ pub fn addable_effects() -> Vec { } else { meta.name.to_string() }; + // The library shows the pack's `node..name`; a + // type the packs don't know keeps the node's own English name. + let name = crate::i18n::tr_or(&node_name_key(meta.type_id), &name); + let group = meta + .categories + .first() + .map(|category| category_group_key(*category)) + .unwrap_or("general"); out.push(EffectEntry { type_id: meta.type_id.to_string(), name, - group: None, + group: Some(group.to_string()), }); } } @@ -136,18 +229,30 @@ pub fn addable_effects() -> Vec { group: Some(meta.sub_category), }); } - // Default presentation order: built-ins first, then the OpenFX - // sub-category groups alphabetically; names alphabetical (folded) - // within each group. + // Default presentation order: built-in groups first, in GROUP_KEYS + // order (`general` last), then the OpenFX sub-category groups + // alphabetically; names alphabetical (folded) within each group. out.sort_by(|a, b| { let ga = a.group.as_deref().unwrap_or(""); let gb = b.group.as_deref().unwrap_or(""); - ga.cmp(gb) - .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())) + match (group_rank(ga), group_rank(gb)) { + (Some(ra), Some(rb)) => ra.cmp(&rb), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => ga.cmp(gb), + } + .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())) }); out } +/// The presentation rank of a built-in group key, or `None` for a key +/// outside [`GROUP_KEYS`] (an OpenFX sub-category, which the sort orders +/// after every built-in group). +fn group_rank(group: &str) -> Option { + GROUP_KEYS.iter().position(|key| *key == group) +} + // --------------------------------------------------------------------------- // OFX parameter data model (stage 6b) // --------------------------------------------------------------------------- @@ -176,6 +281,13 @@ pub fn effect_params( use oak_node::value::ValueType; let entry = g.get(node)?; + // The pack keys describe the built-in nodes only: an OpenFX plugin's + // labels come from the plugin (and the OFX translation pass), and a + // plugin type id happens to end in a built-in type's suffix, the + // built-in name would otherwise leak into it. + let builtin = oak_node::factory::Factory::global() + .find(entry.behavior.type_id()) + .is_some(); let mut out = Vec::new(); for input in &entry.core.inputs { // Clip/texture/sample/matrix inputs are graph connections or @@ -202,6 +314,18 @@ pub fn effect_params( } else { input.display_name.clone() }; + // Built-in nodes label their inputs from the pack instead + // (`node..input.`), so an input the + // behavior names in English — or leaves at its raw input id — + // follows the active language. The behavior's name is the fallback. + let display_name = if builtin { + crate::i18n::tr_or( + &input_name_key(entry.behavior.type_id(), &input.id), + &display_name, + ) + } else { + display_name + }; // Combo options: built-in nodes carry them on the behavior (C++ // `set_combo_box_strings`); OFX plugin params already carry the // ("combo_option", _) properties from the translation pass. @@ -215,6 +339,35 @@ pub fn effect_params( )); } } + // The v3 text node's `font_family_in` is a str-combo whose options + // the node layer cannot supply: oak-node deliberately links no font + // or shaping crate, so the app's text backend enumerates the system + // families and the facade injects them here (the same + // `("combo_option", Text)` shape the OFX translation pass uses). + if input.id == "font_family_in" && !properties.iter().any(|(k, _)| k == "combo_option") { + for name in super::textengine::font_families() { + properties.push(( + "combo_option".to_string(), + oak_node::value::NodeValue::Text(name), + )); + } + } + // The v3 text node's editable text inputs hold more than one line + // (the node shapes each `\n`-separated line of the string), and the + // inspector renders them as multi-line fields so the user can type + // line breaks. The node layer cannot say so — an input carries no + // presentation metadata beyond its properties — so the facade marks + // the multi-line text ids here, the same way it injects the font + // families above. + if input.value_type == ValueType::Text + && matches!(input.id.as_str(), "plain_text_in" | "text_in") + && !properties.iter().any(|(k, _)| k == "multiline") + { + properties.push(( + "multiline".to_string(), + oak_node::value::NodeValue::Boolean(true), + )); + } out.push(super::engine::EffectParam { input_id: input.id.clone(), display_name, @@ -780,21 +933,146 @@ mod tests { } } - /// The effect-library grouping: every addable effect is either an - /// ungrouped built-in or an OpenFX entry with one of the four - /// sub-categories. + /// Every built-in entry carries its category's group key — one of + /// [`GROUP_KEYS`], never a display string — and the shipped nodes use + /// more than one group (a single "general" for everything would mean + /// the category mapping is dead). + #[test] + fn addable_effects_group_builtins_by_category() { + let factory = oak_node::factory::Factory::global(); + let mut groups: Vec<&'static str> = Vec::new(); + let mut builtins = 0usize; + for entry in addable_effects() { + if factory.find(&entry.type_id).is_none() { + continue; // an OpenFX plugin entry + } + builtins += 1; + let group = entry + .group + .as_deref() + .unwrap_or_else(|| panic!("built-in {} has no group", entry.type_id)); + let key = GROUP_KEYS.iter().copied().find(|key| *key == group).unwrap_or_else(|| { + panic!("built-in {} has the unknown group {group:?}", entry.type_id) + }); + if !groups.contains(&key) { + groups.push(key); + } + } + assert!(builtins > 0, "the factory ships built-in video effects"); + assert!( + groups.len() > 1, + "built-ins spread over several groups: {groups:?}" + ); + } + + /// OpenFX plugin entries keep their plugin-supplied sub-category + /// (Filter / Generator / Transition / General), which never collides + /// with a built-in group key. #[test] fn addable_effects_group_openfx_by_sub_category() { + let factory = oak_node::factory::Factory::global(); for entry in addable_effects() { - if let Some(group) = entry.group { + if factory.find_dynamic(&entry.type_id).is_none() { + continue; // a built-in + } + let group = entry + .group + .as_deref() + .expect("an OpenFX entry carries a sub-category"); + assert!( + ["Filter", "Generator", "Transition", "General"].contains(&group), + "unexpected OpenFX sub-category {group:?}" + ); + assert!( + !GROUP_KEYS.contains(&group), + "OpenFX sub-category {group:?} collides with a built-in group key" + ); + } + } + + /// The sort the library and the inspector render: built-in groups in + /// [`GROUP_KEYS`] order, then the OpenFX sub-categories, names folded + /// alphabetically inside a group. + #[test] + fn addable_effects_sort_builtins_first() { + let entries = addable_effects(); + let rank = |entry: &crate::oakui::engine::EffectEntry| { + entry + .group + .as_deref() + .and_then(group_rank) + .unwrap_or(GROUP_KEYS.len()) + }; + for pair in entries.windows(2) { + let (a, b) = (&pair[0], &pair[1]); + let (ra, rb) = (rank(a), rank(b)); + if ra != rb { assert!( - ["Filter", "Generator", "Transition", "General"].contains(&group.as_str()), - "unexpected OpenFX sub-category {group:?}" + ra < rb, + "group order: {a:?} ({ra}) before {b:?} ({rb})" + ); + } else if a.group == b.group { + assert!( + a.name.to_lowercase() <= b.name.to_lowercase(), + "name order inside {:?}: {a:?} before {b:?}", + a.group + ); + } else { + // Two OpenFX sub-categories (rank only separates built-ins). + assert!( + a.group.as_deref().unwrap_or("") <= b.group.as_deref().unwrap_or(""), + "OpenFX sub-category order: {a:?} before {b:?}" ); } } } + /// The category → group-key mapping covers every `Category` variant + /// and falls back to `general`. + #[test] + fn category_group_key_maps_every_category() { + use oak_node::node::Category; + let all = [ + Category::Output, + Category::Effect, + Category::Generator, + Category::Input, + Category::Math, + Category::Color, + Category::Distort, + Category::Filter, + Category::Keying, + Category::OpenFx, + Category::Timeline, + Category::Group, + ]; + for category in all { + assert!( + GROUP_KEYS.contains(&category_group_key(category)), + "{category:?} maps outside GROUP_KEYS" + ); + } + assert_eq!(category_group_key(Category::Color), "color"); + assert_eq!(category_group_key(Category::Keying), "keying"); + assert_eq!(category_group_key(Category::Effect), "general"); + assert_eq!(category_group_key(Category::OpenFx), "general"); + } + + /// A group label is the language pack's string for the built-in keys — + /// never the raw key — and passes OpenFX sub-categories through. + #[test] + fn group_label_localizes_keys_and_passes_sub_categories_through() { + for key in GROUP_KEYS { + let label = group_label(key); + assert!( + !label.starts_with("effect_library.group."), + "language pack entry missing for the {key} group (got {label:?})" + ); + } + assert_eq!(group_label("Filter"), "Filter"); + assert_eq!(group_label("Sapphire"), "Sapphire"); + } + /// `set_input_value` writes the standard value undoably and rejects /// unknown input ids. #[test] @@ -1072,4 +1350,180 @@ mod tests { }; assert_eq!(ui_section_of(&grouped), Some(("Basic".into(), "Main".into()))); } + + /// W6: the v3 text node is a footage entry now (the project panel's + /// "add text footage" button and the timeline drop), so the effect + /// library / add menus no longer offer it. A text3 node that is in a + /// project anyway — a legacy chain, or the one the footage path + /// creates — must keep chaining and keep exposing its params. + #[test] + fn text3_is_not_addable_but_a_chain_node_still_reports_its_params() { + let _g = stack_lock(); + oak_undo::global::clear().unwrap(); + + const TEXT3: &str = "org.olivevideoeditor.Olive.text3"; + assert!( + !addable_effects().iter().any(|e| e.type_id == TEXT3), + "text3 left the effect add menus" + ); + + // `insert` resolves through the factory (which still registers the + // type) rather than the addable table: legacy chains and the + // footage path keep working. + let (project, host) = project_with_clip(); + let node = insert(&project, host, 0, TEXT3).expect("chain a legacy text3 effect"); + assert_eq!(chain(&lock(&project).graph, host), vec![node]); + + let params = effect_params(&lock(&project).graph, node).expect("the node exposes params"); + let plain_text = params + .iter() + .find(|p| p.input_id == "plain_text_in") + .expect("the structured text parameter survives"); + assert_eq!(plain_text.value_type, oak_node::value::ValueType::Text); + // The facade marks the multi-line text ids (the inspector renders a + // multi-line field off this property); the node itself carries no + // presentation metadata. + assert!( + plain_text + .properties + .iter() + .any(|(k, v)| k == "multiline" + && matches!(v, oak_node::value::NodeValue::Boolean(true))), + "plain_text_in is marked as multi-line" + ); + assert!(params.iter().any(|p| p.input_id == "font_size_in")); + assert!(params.iter().any(|p| p.input_id == "outline_enabled_in")); + // The hidden legacy HTML input stays a param *input* in the node, + // never in the inspector. + assert!(!params.iter().any(|p| p.input_id == "text_in")); + + oak_undo::global::clear().unwrap(); + } + + /// The effect library and the inspector name built-in effects and their + /// parameters from runtime-built pack keys (`node..name` + /// / `node..input.`): switching the active + /// language renames the whole library and parameter list, while a node + /// the packs don't know keeps the name it ships with. + #[test] + fn effect_and_parameter_names_follow_the_language_pack() { + let _lang = crate::i18n::lang_test_lock() + .lock() + .unwrap_or_else(|e| e.into_inner()); + let _g = stack_lock(); + oak_undo::global::clear().unwrap(); + crate::i18n::set_language_code("zh-CN"); + + // Every addable built-in shows its pack name, never the factory's + // English one. + let factory = oak_node::factory::Factory::global(); + let mut builtins = 0usize; + for entry in addable_effects() { + let Some(meta) = factory.find(&entry.type_id) else { + continue; // an OpenFX plugin entry: the plugin names it + }; + assert_ne!( + entry.name, meta.name, + "{} is still English in zh-CN", + entry.type_id + ); + builtins += 1; + } + assert!(builtins > 0, "the factory ships addable built-ins"); + let name_of = |type_id: &str| { + addable_effects() + .into_iter() + .find(|e| e.type_id == type_id) + .unwrap_or_else(|| panic!("{type_id} is addable")) + .name + }; + assert_eq!( + name_of("org.olivevideoeditor.Olive.colorcorrect"), + "颜色校正" + ); + assert_eq!(name_of("org.olivevideoeditor.Olive.position"), "位置"); + + // The inspector's parameter labels follow the same keys. + let (project, host) = project_with_clip(); + let node = insert(&project, host, 0, "org.olivevideoeditor.Olive.colorcorrect") + .expect("chain Color Correct"); + let params = effect_params(&lock(&project).graph, node).expect("the node exposes params"); + let label = |input_id: &str| { + params + .iter() + .find(|p| p.input_id == input_id) + .unwrap_or_else(|| panic!("{input_id} is a parameter")) + .display_name + .clone() + }; + assert_eq!(label("saturation_in"), "饱和度"); + assert_eq!(label("gamma_in"), "伽马"); + assert_eq!(label("gain_in"), "增益"); + + crate::i18n::set_language_code("en-US"); + oak_undo::global::clear().unwrap(); + } + + /// The pack coverage behind the lookup above: every name a built-in + /// node gives one of its inputs has a `node..input.` + /// entry, so no parameter label falls back to English. Inputs the + /// behavior does not name (`enabled_in`, `use_args_in`) carry no entry — + /// their label is the raw input id, and the enabled input never renders. + #[test] + fn every_builtin_parameter_name_has_a_pack_entry() { + let _lang = crate::i18n::lang_test_lock() + .lock() + .unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("zh-CN"); + let mut missing = Vec::new(); + let mut named = 0usize; + for meta in oak_node::factory::Factory::global().entries() { + let (core, behavior) = (meta.create)(); + for input in &core.inputs { + let english = behavior.input_name(&input.id); + if english == input.id { + continue; + } + if crate::i18n::tr_or(&input_name_key(meta.type_id, &input.id), english) == english { + missing.push(format!("node.{}.input.{}", type_suffix(meta.type_id), input.id)); + } + named += 1; + } + } + assert!(missing.is_empty(), "no pack entry for {missing:?}"); + assert!(named >= 250, "the built-in nodes name their inputs ({named})"); + crate::i18n::set_language_code("en-US"); + } + + /// The v3 text node's `font_family_in` is a str-combo the node layer + /// cannot populate (oak-node links no font crate): `effect_params` + /// injects the app's font backend families as its combo options, sorted + /// and de-duplicated. + #[test] + fn text_font_family_combo_is_injected_from_the_font_backend() { + let _g = stack_lock(); + let (project, _clip) = project_with_clip(); + let node = { + let mut g = lock(&project); + let (core, behavior) = oak_node::factory::Factory::global() + .create_any("org.olivevideoeditor.Olive.text3") + .expect("the factory registers the v3 text node"); + g.graph.add_node(core, behavior) + }; + + let params = effect_params(&lock(&project).graph, node).expect("the node exposes params"); + let param = params + .iter() + .find(|p| p.input_id == "font_family_in") + .expect("the font family combo is a parameter"); + assert_eq!(param.value_type, oak_node::value::ValueType::StrCombo); + + let options = combo_options(param); + assert!(!options.is_empty(), "the font backend found no families"); + assert_eq!(options, crate::oakui::textengine::font_families()); + let mut sorted = options.clone(); + sorted.sort(); + sorted.dedup(); + assert_eq!(options, sorted); + } } diff --git a/crates/oak-app/src/oakui/engine.rs b/crates/oak-app/src/oakui/engine.rs index f69e575ba..e7ad5a600 100644 --- a/crates/oak-app/src/oakui/engine.rs +++ b/crates/oak-app/src/oakui/engine.rs @@ -339,6 +339,20 @@ pub trait AppEngine: /// it). fn remove_track(&mut self, index: usize, cx: &mut Context); + /// Adds an adjustment layer to the display track at `index`, starting + /// at `frame` and five seconds long (undoable where the backend + /// supports it). Returns a user-facing error message on failure. + /// Default: unsupported. + fn add_adjustment_layer( + &mut self, + index: usize, + frame: Frame, + cx: &mut Context, + ) -> Result<(), String> { + let _ = (index, frame, cx); + Err("add adjustment layer not supported".into()) + } + /// Sets the row height of every timeline track (timeline toolbar). fn set_track_height(&mut self, height: Pixels, cx: &mut Context); @@ -1071,6 +1085,15 @@ pub trait AppEngine: Err("no project open".to_string()) } + /// Creates a text generator in the open project's root folder (the + /// project explorer's "添加文本素材" action) and returns its entry id. + /// The bin entry is NOT a sequence: a double-click selects it like + /// footage, and the timeline drop places a generator clip. + fn create_text_footage(&mut self, cx: &mut Context) -> Result { + let _ = cx; + Err("no project open".to_string()) + } + /// Applies new name/format/interlaced parameters to the sequence `id`. fn update_sequence_parameters( &mut self, @@ -1213,6 +1236,18 @@ pub trait AppEngine: let _ = (clips, cx); } + /// Adds a transition of the configured default length (the + /// `DefaultTransitionLength` preference, seconds) at every contiguous + /// seam around `clips` — the clip menu's Default Transition item and the + /// Ctrl+Shift+D action (the C++ `timeline::add_default_transition`). + /// One undoable "Add Transition" entry; returns how many transitions + /// were created, or `Err` when no selected seam accepts one. + fn add_default_transition( + &mut self, + clips: Vec, + cx: &mut Context, + ) -> Result; + // ------------------------------------------------------------------- // Multi-camera (the C++ MulticamWidget / timeline Multi-Cam menu): // detection state for the panel, angle-frame rendering, the timeline diff --git a/crates/oak-app/src/oakui/graphops.rs b/crates/oak-app/src/oakui/graphops.rs index a5a74666c..b9d1f59c4 100644 --- a/crates/oak-app/src/oakui/graphops.rs +++ b/crates/oak-app/src/oakui/graphops.rs @@ -33,10 +33,10 @@ use std::path::Path; use std::sync::{Arc, Mutex, MutexGuard}; use oak_core::{Rational, TimeRange}; -use oak_node::block::ClipBlockBehavior; +use oak_node::block::{AdjustmentBlockBehavior, BlockCore, ClipBlockBehavior}; use oak_node::folder::FolderBehavior; use oak_node::footage::FootageBehavior; -use oak_node::graph::Graph; +use oak_node::graph::{Graph, NodeEntry}; use oak_node::id::NodeId; use oak_node::project::Project; use oak_node::sequence::SequenceBehavior; @@ -296,6 +296,96 @@ pub fn create_folder(project: &ProjectRef, name: &str) -> Result Ok(id) } +/// The stable type id of the text generator node (the redesigned `textv3`, +/// "org.olivevideoeditor.Olive.text3"): the bin entry the project panel's +/// "添加文本素材" action creates, and the type the timeline drop routes to +/// the generator-clip path instead of the footage path. +pub const TEXT_FOOTAGE_TYPE_ID: &str = "org.olivevideoeditor.Olive.text3"; + +/// The bin label a created text generator starts with (the plan's "文本" +/// entry name). Fixed, not localized at creation time: it is the node's +/// label, which the user renames like any other bin entry. +pub const TEXT_FOOTAGE_LABEL: &str = "文本"; + +/// Create a text generator in the project's root folder (the project +/// panel's "添加文本素材" action). ONE undo row covers the node AND its +/// mount: the undo detaches the node from the root folder and removes it +/// from the graph (its entry stays inside the command), so no orphan node +/// outlives the undo — unlike [`create_folder`], which leaves its node in +/// the graph and only undoes the mount. The redo re-inserts the entry, +/// which keeps its stable id while its slot is free (the +/// [`Graph::add_entry`] contract). +pub fn create_text_footage_node(project: &ProjectRef) -> Result { + let root = { + let guard = lock(project); + if !guard.root.valid() { + return Err("the project has no root folder".to_string()); + } + guard.root + }; + let Some((mut core, behavior)) = + oak_node::factory::Factory::global().create_any(TEXT_FOOTAGE_TYPE_ID) + else { + return Err(format!( + "the text node \"{TEXT_FOOTAGE_TYPE_ID}\" is not registered" + )); + }; + core.label = TEXT_FOOTAGE_LABEL.to_string(); + // The eager insert gives the caller an id for the bin entry before the + // command is built; the undo parks the entry here between runs. + let id = { + let mut guard = lock(project); + guard.graph.add_node(core, behavior) + }; + let detached: Arc>> = Arc::new(Mutex::new(None)); + let (redo_detached, undo_detached) = (detached.clone(), detached.clone()); + let (redo_project, undo_project) = (project.clone(), project.clone()); + let redo = move || { + let mut guard = lock(&redo_project); + if !guard.graph.is_valid(id) { + let entry = redo_detached + .lock() + .unwrap_or_else(|e| e.into_inner()) + .take(); + if let Some(entry) = entry { + guard.graph.add_entry(entry, id); + } + } + if let Some(entry) = guard.graph.get_mut(id) { + entry.core.bin_folder = Some(root); + } + if let Some(entry) = guard.graph.get_mut(root) { + if let Some(folder) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + folder.add_child(id); + } + } + }; + let undo = move || { + let mut guard = lock(&undo_project); + if let Some(entry) = guard.graph.get_mut(root) { + if let Some(folder) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + folder.remove_child(id); + } + } + if let Some(entry) = guard.graph.get_mut(id) { + entry.core.bin_folder = None; + } + let taken = guard.graph.take_node(id); + *undo_detached.lock().unwrap_or_else(|e| e.into_inner()) = taken; + }; + let cmd = oak_undo::undocommand::UndoCommand::from_closures(redo, undo); + push_command(cmd, "Add Text Footage")?; + Ok(id) +} + /// Every folder node in the graph, in arena order. pub fn folder_ids(p: &Project) -> Vec { p.graph @@ -455,7 +545,7 @@ pub fn sequence_length(g: &Graph, seq: NodeId) -> Rational { } /// Borrow a block's core (any block kind). -fn block_core(entry: &oak_node::graph::NodeEntry) -> Option<&oak_node::block::BlockCore> { +fn block_core(entry: &oak_node::graph::NodeEntry) -> Option<&BlockCore> { let any = entry.behavior.as_any()?; if let Some(c) = any.downcast_ref::() { return Some(&c.core); @@ -463,8 +553,77 @@ fn block_core(entry: &oak_node::graph::NodeEntry) -> Option<&oak_node::block::Bl if let Some(g) = any.downcast_ref::() { return Some(&g.core); } - any.downcast_ref::() - .map(|t| &t.core) + if let Some(t) = any.downcast_ref::() { + return Some(&t.core); + } + any.downcast_ref::() + .map(|a| &a.core) +} + +/// Borrow a block's core mutably (any block kind). +fn block_core_mut(entry: &mut oak_node::graph::NodeEntry) -> Option<&mut BlockCore> { + let any = entry.behavior.as_any_mut()?; + if any.is::() { + Some( + &mut any + .downcast_mut::() + .expect("checked above") + .core, + ) + } else if any.is::() { + Some( + &mut any + .downcast_mut::() + .expect("checked above") + .core, + ) + } else if any.is::() { + Some( + &mut any + .downcast_mut::() + .expect("checked above") + .core, + ) + } else if any.is::() { + Some( + &mut any + .downcast_mut::() + .expect("checked above") + .core, + ) + } else { + None + } +} + +/// Borrow the block core at `node` (any block kind). +pub fn block_core_of(g: &Graph, node: NodeId) -> Option<&BlockCore> { + block_core(g.get(node)?) +} + +/// Mutate the block core at `node` (any block kind). Returns `false` and +/// leaves the graph untouched when `node` is not a block. +fn with_block_core_mut(g: &mut Graph, node: NodeId, f: impl FnOnce(&mut BlockCore)) -> bool { + let Some(entry) = g.get_mut(node) else { + return false; + }; + let Some(core) = block_core_mut(entry) else { + return false; + }; + f(core); + true +} + +/// Whether `node` is a clip-class block: a clip or an adjustment layer. +/// Gaps and transitions are blocks too, but not clips. +fn is_timeline_clip(g: &Graph, node: NodeId) -> bool { + let Some(entry) = g.get(node) else { + return false; + }; + let Some(any) = entry.behavior.as_any() else { + return false; + }; + any.is::() || any.is::() } /// The sequence playhead (rational seconds). @@ -592,28 +751,29 @@ pub fn track_ids(g: &Graph, seq: NodeId, kind: TrackType) -> Vec { .unwrap_or_default() } -/// The clip blocks of a track (gaps skipped), in timeline order. +/// The clip blocks of a track, in timeline order. Gaps and transitions are +/// not clips and are skipped; adjustment layers are. pub fn clip_ids(g: &Graph, track: NodeId) -> Vec { track_behavior(g, track) .map(|t| { t.blocks .iter() .copied() - .filter(|&b| clip_behavior(g, b).is_some()) + .filter(|&b| is_timeline_clip(g, b)) .collect() }) .unwrap_or_default() } -/// A clip's timeline range and media in-point (rational seconds). +/// A block's timeline range and media in-point (rational seconds). pub fn clip_range(g: &Graph, clip: NodeId) -> Option<(Rational, Rational, Rational)> { - let c = clip_behavior(g, clip)?; - Some((c.core.in_(), c.core.out(), c.core.media_in)) + let c = block_core_of(g, clip)?; + Some((c.in_(), c.out(), c.media_in)) } -/// The clip's owning track. +/// The block's owning track. pub fn clip_track(g: &Graph, clip: NodeId) -> Option { - clip_behavior(g, clip)?.core.track + block_core_of(g, clip)?.track } /// The first footage node feeding `id` (upstream BFS over input edges), @@ -1505,6 +1665,401 @@ pub fn place_footage_clip( Ok(clip) } +/// Add an adjustment layer to `track` spanning `[in_ts, out_ts)` (frame +/// timestamps in the sequence's timebase): an [`AdjustmentBlockBehavior`] +/// block with no footage, so it renders its effect chain over the clips +/// underneath it. Only video tracks take adjustment layers. +/// +/// Undoable "Add Adjustment Layer" (the block is created in the project +/// and placed by the module's `TrackPlaceBlockCommand`); on undo the +/// block is detached from the track but left as an orphan node in the +/// project graph (the module command's documented behavior, same as +/// [`place_footage_clip`]). +pub fn create_adjustment_layer( + p: &ProjectRef, + seq: NodeId, + track: NodeId, + in_ts: i64, + out_ts: i64, +) -> Result { + if in_ts < 0 || out_ts <= in_ts { + return Err("invalid adjustment layer range (need 0 <= in < out)".to_string()); + } + let (list, track_index, in_r, out_r) = { + let g = lock(p); + let behavior = track_behavior(&g.graph, track) + .ok_or_else(|| "the track is not in the project".to_string())?; + if behavior.kind != TrackType::Video { + return Err("adjustment layers are only supported on video tracks".to_string()); + } + let tb = sequence_time_base(&g.graph, seq) + .ok_or_else(|| "sequence has no valid frame rate".to_string())?; + let list = behavior + .track_list + .ok_or_else(|| "the track is not in a track list".to_string())?; + let index = track_list_behavior(&g.graph, list) + .and_then(|l| l.tracks.iter().position(|&t| t == track)) + .ok_or_else(|| "the track is not in its list".to_string())?; + (list, index, ts_to_rational(in_ts, tb), ts_to_rational(out_ts, tb)) + }; + + // The block itself: no label, no color override, no media wiring — + // just its timeline span. Its placement below is the undoable part. + let block = { + let mut g = lock(p); + let (core, behavior) = oak_node::block::adjustment_create(); + let block = g.graph.add_node(core, behavior); + with_block_core_mut(&mut g.graph, block, |core| { + core.range = TimeRange::new(in_r, out_r); + }); + block + }; + + push( + oak_timeline::undopointer::TrackPlaceBlockCommand::new( + node_ref(p, list), + track_index as i32, + node_ref(p, block), + in_r, + ) + .to_command(), + "Add Adjustment Layer", + )?; + Ok(block) +} + +/// The transition block joining `prev` to `next` on `track`, when one is +/// already wired between them. +fn transition_between(g: &Graph, track: NodeId, prev: NodeId, next: NodeId) -> Option { + track_behavior(g, track) + .map(|t| t.blocks.clone()) + .unwrap_or_default() + .into_iter() + .find(|&b| { + g.get(b) + .and_then(|e| e.behavior.as_any()) + .map(|a| a.is::()) + .unwrap_or(false) + && g.connected_output(b, oak_node::block::transition_input::OUT_BLOCK, -1) + == Some(prev) + && g.connected_output(b, oak_node::block::transition_input::IN_BLOCK, -1) + == Some(next) + }) +} + +/// Build (without pushing) the commands that create a transition block at +/// the seam between `prev` and `next` and wire both sides into it. +/// +/// The block covers `[out(prev) - half, out(prev) + half]`: `in_offset` +/// reaches into `prev` and `out_offset` into `next` (the module's +/// transition geometry), both `half` for a symmetric default transition. +/// `prev` and `next` must be clip blocks listed back to back on one track +/// and must touch exactly. Everything is validated before the block node +/// is created, so a rejected seam leaves the graph untouched. +/// +/// The block enters the track through the module's non-destructive +/// `TrackInsertBlockAfterCommand`; `TrackPlaceBlockCommand` is unusable +/// here because its redo ripple-removes the span the new block covers, +/// which would trim away both clips the transition straddles. +fn transition_commands( + p: &ProjectRef, + prev: NodeId, + next: NodeId, + half: Rational, +) -> Result<(NodeId, Vec), String> { + if half <= Rational::new(0, 1) { + return Err("transition: the length must be positive".to_string()); + } + let (seam, owner) = { + let g = lock(p); + if clip_behavior(&g.graph, prev).is_none() { + return Err("transition: the outgoing block is not a clip".to_string()); + } + if clip_behavior(&g.graph, next).is_none() { + return Err("transition: the incoming block is not a clip".to_string()); + } + let Some(prev_core) = block_core_of(&g.graph, prev) else { + return Err("transition: the outgoing clip has no block core".to_string()); + }; + let Some(next_core) = block_core_of(&g.graph, next) else { + return Err("transition: the incoming clip has no block core".to_string()); + }; + if next_core.in_() != prev_core.out() { + return Err("transition: the clips are not contiguous at the seam".to_string()); + } + let Some(track) = prev_core.track else { + return Err("transition: the outgoing clip is not on a track".to_string()); + }; + if next_core.track != Some(track) { + return Err("transition: the clips are not on the same track".to_string()); + } + let Some(blocks) = track_behavior(&g.graph, track).map(|t| t.blocks.clone()) else { + return Err("transition: the track is not in the project".to_string()); + }; + let adjacent = blocks + .iter() + .position(|&b| b == prev) + .and_then(|i| blocks.get(i + 1).copied()) + == Some(next); + if !adjacent { + return Err("transition: the clips are not adjacent on the track".to_string()); + } + if transition_between(&g.graph, track, prev, next).is_some() { + return Err("transition: the seam already carries a transition".to_string()); + } + (prev_core.out(), track) + }; + + let block = { + let mut g = lock(p); + let (core, behavior) = oak_node::block::transition_create(); + let id = g.graph.add_node(core, behavior); + let Some(t) = g + .graph + .get_mut(id) + .and_then(|e| e.behavior.as_any_mut()) + .and_then(|a| a.downcast_mut::()) + else { + return Err("transition: could not create the block".to_string()); + }; + t.core.range = TimeRange::new(seam - half, seam + half); + t.in_offset = half; + t.out_offset = half; + id + }; + let insert = oak_timeline::undotrack::TrackInsertBlockAfterCommand::new( + node_ref(p, owner), + node_ref(p, block), + Some(node_ref(p, prev)), + ) + .to_command(); + let from = connect_command(p, prev, block, oak_node::block::transition_input::OUT_BLOCK)?; + let to = connect_command(p, next, block, oak_node::block::transition_input::IN_BLOCK)?; + Ok((block, vec![insert, from, to])) +} + +/// Create a transition at the seam between two contiguous clips and push +/// it as one "Add Transition" undo row (the timeline's wedge drag path). +pub fn add_transition_at_seam( + p: &ProjectRef, + prev: NodeId, + next: NodeId, + half: Rational, +) -> Result { + let (block, commands) = transition_commands(p, prev, next, half)?; + push_multi(commands, "Add Transition")?; + Ok(block) +} + +/// Add a default transition at every contiguous seam around the selected +/// clips (the Ctrl+Shift+D action / the timeline clip-menu item). +/// +/// `half` is half of the default transition length: the transition spans +/// `half` on either side of its seam. Every selected clip contributes the +/// seam before it and the seam after it; a neighbor only counts when it is +/// a clip block (not a gap, a transition or an adjustment layer) that +/// touches the clip exactly, and a seam shared by two selected clips is +/// built once. Seams that cannot take a transition (an existing +/// transition, a non-contiguous neighbor) are skipped. The whole batch is +/// one "Add Transition" undo row; a batch that builds nothing is an error, +/// so the action never leaves an empty row. Returns the number of +/// transitions created. +/// +/// `seq` scopes the action: a selected block whose track list belongs to +/// another sequence is ignored. +pub fn add_default_transition( + p: &ProjectRef, + seq: NodeId, + clip_blocks: &[NodeId], + half: Rational, +) -> Result { + let lists = { + let g = lock(p); + let Some(s) = sequence_behavior(&g.graph, seq) else { + return Err("transition: the sequence is not in the project".to_string()); + }; + s.track_lists.clone() + }; + + let mut seams: Vec<(NodeId, NodeId)> = Vec::new(); + for &clip in clip_blocks { + let candidates: Vec<(NodeId, NodeId)> = { + let g = lock(p); + if clip_behavior(&g.graph, clip).is_none() { + continue; + } + let Some(track) = clip_track(&g.graph, clip) else { + continue; + }; + let Some(list) = track_behavior(&g.graph, track).and_then(|t| t.track_list) else { + continue; + }; + if !lists.contains(&list) { + continue; + } + let Some(blocks) = track_behavior(&g.graph, track).map(|t| t.blocks.clone()) else { + continue; + }; + let Some(index) = blocks.iter().position(|&b| b == clip) else { + continue; + }; + let range = block_core_of(&g.graph, clip).map(|c| (c.in_(), c.out())); + let mut sides = Vec::new(); + if let Some(&prev) = index.checked_sub(1).and_then(|i| blocks.get(i)) { + if clip_behavior(&g.graph, prev).is_some() + && block_core_of(&g.graph, prev).map(|c| c.out()) == range.map(|r| r.0) + { + sides.push((prev, clip)); + } + } + if let Some(&next) = blocks.get(index + 1) { + if clip_behavior(&g.graph, next).is_some() + && block_core_of(&g.graph, next).map(|c| c.in_()) == range.map(|r| r.1) + { + sides.push((clip, next)); + } + } + sides + }; + for seam in candidates { + if !seams.contains(&seam) { + seams.push(seam); + } + } + } + if seams.is_empty() { + return Err("transition: no selected clip borders another clip".to_string()); + } + + let mut commands = Vec::new(); + let mut created = 0usize; + for (prev, next) in seams { + if let Ok((_, children)) = transition_commands(p, prev, next, half) { + commands.extend(children); + created += 1; + } + } + if created == 0 { + return Err("transition: no contiguous seam accepts a transition".to_string()); + } + push_multi(commands, "Add Transition")?; + Ok(created) +} + +/// The transition block drawn on one edge of `clip`, when its neighbour on +/// that side is a transition wired into the clip. +/// +/// The timeline widget addresses a wedge through the clip it is drawn on: +/// the start wedge belongs to the transition *before* the clip (the clip +/// feeds its `in_block_in`) and the end wedge to the one *after* it +/// (`out_block_in`). +pub fn transition_of_clip(g: &Graph, clip: NodeId, start_edge: bool) -> Option { + let track = clip_track(g, clip)?; + let blocks = track_behavior(g, track)?.blocks.clone(); + let index = blocks.iter().position(|&b| b == clip)?; + let candidate = if start_edge { + blocks.get(index.checked_sub(1)?).copied()? + } else { + blocks.get(index + 1).copied()? + }; + let input = if start_edge { + oak_node::block::transition_input::IN_BLOCK + } else { + oak_node::block::transition_input::OUT_BLOCK + }; + if g.connected_output(candidate, input, -1) == Some(clip) { + Some(candidate) + } else { + None + } +} + +/// Resize one wedge of the transition attached to `clip` (the timeline +/// widget's wedge drag, its `TransitionChanged` event). +/// +/// `start_edge` names the wedge by the clip it is drawn on: the start wedge +/// is the incoming transition's `out_offset`, the end wedge the outgoing +/// one's `in_offset`. The width is clamped to `frame..=clip length` — one +/// frame at the smallest, and never wider than the clip it eats into — and +/// written through [`oak_timeline::undogeneral::TransitionSetOffsetsCommand`], +/// so one "Transition Length" undo row covers the drag. The seam does not +/// move. Returns the width actually written. +pub fn set_transition_length( + p: &ProjectRef, + clip: NodeId, + start_edge: bool, + frame: Rational, + new_length: Rational, +) -> Result { + let (transition, clip_length) = { + let g = lock(p); + let Some(transition) = transition_of_clip(&g.graph, clip, start_edge) else { + return Err("transition: the clip has no transition on that edge".to_string()); + }; + let Some(core) = block_core_of(&g.graph, clip) else { + return Err("transition: the clip has no block core".to_string()); + }; + (transition, core.length()) + }; + let clamped = new_length.max(frame).min(clip_length); + push_command( + oak_timeline::undogeneral::TransitionSetOffsetsCommand::new( + node_ref(p, transition), + if start_edge { None } else { Some(clamped) }, + if start_edge { Some(clamped) } else { None }, + ) + .to_command(), + "Transition Length", + )?; + Ok(clamped) +} + +/// The transition wedges of every clip on `track`, keyed by clip: +/// `(start, end)` are the widths of the transition before the clip (drawn +/// on its head) and after it (drawn on its tail); `None` when that side has +/// no transition. Gaps and transitions themselves are not keyed. +pub fn clip_transition_widths( + g: &Graph, + track: NodeId, +) -> std::collections::HashMap, Option)> { + let mut widths: std::collections::HashMap, Option)> = + std::collections::HashMap::new(); + let Some(blocks) = track_behavior(g, track).map(|t| t.blocks.clone()) else { + return widths; + }; + for &block in &blocks { + if is_timeline_clip(g, block) { + widths.insert(block, (None, None)); + } + } + for &block in &blocks { + let Some(transition) = g + .get(block) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + else { + continue; + }; + // A transition overlaps its neighbours: it reaches `in_offset` into + // the block before it and `out_offset` into the block after it. + if let Some(prev) = + g.connected_output(block, oak_node::block::transition_input::OUT_BLOCK, -1) + { + if let Some(pair) = widths.get_mut(&prev) { + pair.1 = Some(transition.in_offset); + } + } + if let Some(next) = + g.connected_output(block, oak_node::block::transition_input::IN_BLOCK, -1) + { + if let Some(pair) = widths.get_mut(&next) { + pair.0 = Some(transition.out_offset); + } + } + } + widths +} + /// Places ONE video clip fed by a sequence node — a nested-sequence clip /// (drag a sequence entry from the project explorer onto the timeline). /// The clip reads the sequence's output during playback; the sequence's @@ -1585,6 +2140,85 @@ pub fn place_nested_sequence_clip( Ok(clip) } +/// Places ONE video clip fed by the text generator `text` (drag a text +/// entry from the project explorer onto the timeline) — a generator clip, +/// not a decode of media: the clip reads the generator's output through +/// `tex_in`, so the text re-renders at whatever time the clip spans. +/// Undoable as one "Add Clip". `in_ts`/`out_ts` are frame timestamps in +/// the host sequence's frame-rate timebase; a clip dragged shorter or +/// longer than the drop's default simply re-spans the same generator. +pub fn place_text_clip( + p: &ProjectRef, + host_seq: NodeId, + text: NodeId, + track_index: usize, + in_ts: i64, + out_ts: i64, +) -> Result { + if in_ts < 0 || out_ts <= in_ts { + return Err("invalid clip range (need 0 <= in < out)".to_string()); + } + let (tb, list, label) = { + let g = lock(p); + if node_type_id(&g.graph, text) != TEXT_FOOTAGE_TYPE_ID { + return Err("the text entry is not in the project".to_string()); + } + let tb = sequence_time_base(&g.graph, host_seq) + .ok_or_else(|| "host sequence has no valid frame rate".to_string())?; + let list = track_list_of(&g.graph, host_seq, TrackType::Video) + .ok_or_else(|| "host sequence has no video track list".to_string())?; + let label = node_label(&g.graph, text); + (tb, list, label) + }; + let track_count = { + let g = lock(p); + track_list_behavior(&g.graph, list) + .map(|l| l.tracks.len()) + .unwrap_or(0) + }; + if track_index >= track_count { + return Err(format!( + "track index {track_index} out of range ({track_count} tracks)" + )); + } + + let in_r = ts_to_rational(in_ts, tb); + let out_r = ts_to_rational(out_ts, tb); + let length = out_r - in_r; + + // The clip block, labelled after its generator and stamped with its + // creation-time color (the footage-clip convention). A generator has no + // media, so only the length is written; `TrackPlaceBlockCommand` sets + // the in-point from the drop. + let clip = oak_timeline::util::block_clip_create(p).id; + { + let mut g = lock(p); + let Some(entry) = g.graph.get_mut(clip) else { + return Err("clip node is not in the project".to_string()); + }; + entry.core.override_color = clip_color_index(clip); + entry.core.label = label; + if let Some(c) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + c.core.set_length_and_media_in(length); + } + } + + let place = oak_timeline::undopointer::TrackPlaceBlockCommand::new( + node_ref(p, list), + track_index as i32, + node_ref(p, clip), + in_r, + ) + .to_command(); + let edge = connect_command(p, text, clip, oak_node::block::clip_input::TEXTURE_INPUT)?; + push_multi(vec![place, edge], "Add Clip")?; + Ok(clip) +} + /// Place linked clips of one footage on several tracks in ONE undoable /// "Add Clip" entry (the NLE A/V-drop: a video-with-audio file lands as /// a video clip plus a linked audio clip). `placements` lists the @@ -1762,38 +2396,27 @@ pub(crate) fn trim_command( old: Rational, new: Rational, ) -> oak_undo::undocommand::UndoCommand { - use oak_node::block::BlockCore; let (p1, p2) = (p.clone(), p.clone()); oak_undo::undocommand::UndoCommand::from_closures( move || { - let mut g = lock(&p1); - if let Some(c) = g - .graph - .get_mut(clip) - .and_then(|e| e.behavior.as_any_mut()) - .and_then(|a| a.downcast_mut::()) - { + let mut guard = lock(&p1); + with_block_core_mut(&mut guard.graph, clip, |core| { if out_anchored { - BlockCore::set_length_and_media_out(&mut c.core, new); + core.set_length_and_media_out(new); } else { - BlockCore::set_length_and_media_in(&mut c.core, new); + core.set_length_and_media_in(new); } - } + }); }, move || { - let mut g = lock(&p2); - if let Some(c) = g - .graph - .get_mut(clip) - .and_then(|e| e.behavior.as_any_mut()) - .and_then(|a| a.downcast_mut::()) - { + let mut guard = lock(&p2); + with_block_core_mut(&mut guard.graph, clip, |core| { if out_anchored { - BlockCore::set_length_and_media_out(&mut c.core, old); + core.set_length_and_media_out(old); } else { - BlockCore::set_length_and_media_in(&mut c.core, old); + core.set_length_and_media_in(old); } - } + }); }, ) } @@ -2164,26 +2787,12 @@ fn move_clip_to_track_commands( let (p1, p2) = (p.clone(), p.clone()); let rehome = oak_undo::undocommand::UndoCommand::from_closures( move || { - let mut g = lock(&p1); - if let Some(c) = g - .graph - .get_mut(clip) - .and_then(|e| e.behavior.as_any_mut()) - .and_then(|a| a.downcast_mut::()) - { - c.core.set_in(in_r); - } + let mut guard = lock(&p1); + with_block_core_mut(&mut guard.graph, clip, |core| core.set_in(in_r)); }, move || { - let mut g = lock(&p2); - if let Some(c) = g - .graph - .get_mut(clip) - .and_then(|e| e.behavior.as_any_mut()) - .and_then(|a| a.downcast_mut::()) - { - c.core.set_in(old_in); - } + let mut guard = lock(&p2); + with_block_core_mut(&mut guard.graph, clip, |core| core.set_in(old_in)); }, ); let place = oak_timeline::undopointer::TrackPlaceBlockCommand::new( @@ -3390,4 +3999,616 @@ mod undo_cycle_ops_tests { assert!(interlaced, "the interlaced flag updates too"); oak_undo::global::clear().unwrap(); } + + // ---- adjustment layers ------------------------------------------------ + + /// Drop a fresh adjustment-layer block onto `track` spanning + /// `[in_ts, out_ts)` (timeline frames) and return its node id. + fn add_adjustment_layer( + p: &ProjectRef, + seq: NodeId, + track: NodeId, + in_ts: i64, + out_ts: i64, + ) -> NodeId { + let tb = { + let g = lock(p); + sequence_time_base(&g.graph, seq).expect("the sequence has a timebase") + }; + let in_r = ts_to_rational(in_ts, tb); + let block = { + let mut g = lock(p); + let (core, behavior) = oak_node::block::adjustment_create(); + let id = g.graph.add_node(core, behavior); + let entry = g.graph.get_mut(id).expect("the new block"); + let a = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + .expect("an adjustment behavior"); + a.core.range = TimeRange::new(in_r, ts_to_rational(out_ts, tb)); + id + }; + let (list, index) = { + let g = lock(p); + let list = track_behavior(&g.graph, track) + .and_then(|t| t.track_list) + .expect("the track has a list"); + let index = track_list_behavior(&g.graph, list) + .and_then(|l| l.tracks.iter().position(|&t| t == track)) + .expect("the track is in its list") as i32; + (list, index) + }; + push( + oak_timeline::undopointer::TrackPlaceBlockCommand::new( + node_ref(p, list), + index, + node_ref(p, block), + in_r, + ) + .to_command(), + "Add Adjustment Layer", + ) + .expect("place the adjustment layer"); + block + } + + /// An adjustment layer is a timeline block: the generic clip queries + /// see it, its length counts toward the track, and the generic trim / + /// delete operations apply to it with converging undo-redo cycles. + #[test] + fn adjustment_layer_trim_and_delete_cycles_converge() { + let _g = test_lock(); + oak_undo::global::clear().unwrap(); + let project = create_project(); + let seq = create_sequence(&project, "Adjustment Ops"); + add_track(&project, seq, TrackType::Video).expect("add video track"); + let track = { + let g = lock(&project); + track_ids(&g.graph, seq, TrackType::Video)[0] + }; + let tb = { + let g = lock(&project); + sequence_time_base(&g.graph, seq).expect("timebase") + }; + let adj = add_adjustment_layer(&project, seq, track, 50, 125); + + { + let g = lock(&project); + assert!( + clip_ids(&g.graph, track).contains(&adj), + "an adjustment layer counts as a timeline block" + ); + assert_eq!( + clip_range(&g.graph, adj), + Some(( + ts_to_rational(50, tb), + ts_to_rational(125, tb), + Rational::new(0, 1) + )), + "the block reports its timeline range and a zero media in" + ); + assert_eq!( + clip_track(&g.graph, adj), + Some(track), + "the block knows its track" + ); + } + assert_eq!( + oak_timeline::util::track_length(&node_ref(&project, track)), + ts_to_rational(125, tb), + "the track length counts the leading gap plus the adjustment layer" + ); + + // Out-trim: the in point stays, the out point moves. + trim_clip(&project, adj, 50, 100).expect("trim the adjustment layer"); + { + let g = lock(&project); + let (in_r, out_r, _) = clip_range(&g.graph, adj).expect("range"); + assert_eq!(in_r, ts_to_rational(50, tb)); + assert_eq!( + out_r, + ts_to_rational(100, tb), + "the out-trim lands on the adjustment layer" + ); + } + let post = snapshot(&project, seq); + cycle_assert(&project, seq, &post, "adjustment trim"); + oak_undo::global::clear().unwrap(); + + // Delete: the block leaves the track (and the clip list), and the + // replaced span is a gap, not a block. + delete_clip(&project, adj).expect("delete the adjustment layer"); + { + let g = lock(&project); + assert!( + !clip_ids(&g.graph, track).contains(&adj), + "the deleted block leaves the clip list" + ); + assert_eq!( + clip_track(&g.graph, adj), + None, + "the removed block no longer names a track" + ); + let blocks = track_behavior(&g.graph, track) + .map(|t| t.blocks.clone()) + .unwrap_or_default(); + assert_eq!( + blocks.len(), + 0, + "the trailing block and the gap that preceded it are gone: {blocks:?}" + ); + } + let post = snapshot(&project, seq); + cycle_assert(&project, seq, &post, "adjustment delete"); + oak_undo::global::clear().unwrap(); + } + + /// The production entry point ([`create_adjustment_layer`]) lands the + /// block on the video track with the requested span (nothing wired to + /// it), the undo detaches it without removing the node and the redo + /// re-places the same block. + #[test] + fn create_adjustment_layer_places_on_video_track_and_undoes() { + let _g = test_lock(); + oak_undo::global::clear().unwrap(); + let project = create_project(); + let seq = create_sequence(&project, "Adjustment UI"); + add_track(&project, seq, TrackType::Video).expect("add video track"); + let track = { + let g = lock(&project); + track_ids(&g.graph, seq, TrackType::Video)[0] + }; + let tb = { + let g = lock(&project); + sequence_time_base(&g.graph, seq).expect("timebase") + }; + + let adj = create_adjustment_layer(&project, seq, track, 50, 125).expect("create"); + { + let g = lock(&project); + assert_eq!( + clip_ids(&g.graph, track), + vec![adj], + "the adjustment layer is the track's only block" + ); + assert_eq!( + clip_range(&g.graph, adj), + Some(( + ts_to_rational(50, tb), + ts_to_rational(125, tb), + Rational::new(0, 1) + )), + "the block spans the requested range with a zero media in" + ); + assert_eq!( + clip_track(&g.graph, adj), + Some(track), + "the block knows its track" + ); + assert!( + g.graph + .get(adj) + .and_then(|e| e.behavior.as_any()) + .map(|a| a.is::()) + .unwrap_or(false), + "the new block is an adjustment layer, not a clip" + ); + } + assert_eq!( + oak_timeline::util::track_length(&node_ref(&project, track)), + ts_to_rational(125, tb), + "the track length counts the leading gap plus the layer" + ); + let post = snapshot(&project, seq); + cycle_assert(&project, seq, &post, "create adjustment"); + + oak_undo::global::undo().unwrap(); + { + let g = lock(&project); + assert_eq!( + clip_track(&g.graph, adj), + None, + "the undo detaches the block from its track" + ); + assert!( + g.graph.is_valid(adj), + "the detached block stays in the graph as an orphan" + ); + } + oak_undo::global::redo().unwrap(); + { + let g = lock(&project); + assert_eq!( + clip_track(&g.graph, adj), + Some(track), + "the redo re-places the same block" + ); + } + oak_undo::global::clear().unwrap(); + } + + /// Only video tracks take adjustment layers, and the span must be a + /// forward range of non-negative frames; a rejected call creates no + /// node and leaves the undo stack untouched. + #[test] + fn create_adjustment_layer_rejects_audio_tracks_and_bad_ranges() { + let _g = test_lock(); + oak_undo::global::clear().unwrap(); + let project = create_project(); + let seq = create_sequence(&project, "Adjustment Rejects"); + add_track(&project, seq, TrackType::Video).expect("add video track"); + add_track(&project, seq, TrackType::Audio).expect("add audio track"); + let (video, audio) = { + let g = lock(&project); + ( + track_ids(&g.graph, seq, TrackType::Video)[0], + track_ids(&g.graph, seq, TrackType::Audio)[0], + ) + }; + let before = oak_undo::global::count().unwrap(); + + assert!( + create_adjustment_layer(&project, seq, audio, 0, 25).is_err(), + "an audio track takes no adjustment layer" + ); + assert!( + create_adjustment_layer(&project, seq, video, 25, 25).is_err(), + "an empty range is rejected" + ); + assert!( + create_adjustment_layer(&project, seq, video, 50, 25).is_err(), + "a reversed range is rejected" + ); + assert!( + create_adjustment_layer(&project, seq, video, -1, 25).is_err(), + "a negative in point is rejected" + ); + assert_eq!( + oak_undo::global::count().unwrap(), + before, + "a rejected call leaves the undo stack untouched" + ); + { + let g = lock(&project); + assert!( + clip_ids(&g.graph, video).is_empty(), + "no block was created for the rejected calls" + ); + } + oak_undo::global::clear().unwrap(); + } + + /// 添加文本素材: the helper creates a text generator mounted under the + /// root as ONE undoable row. The undo detaches it from the folder AND + /// removes the node from the graph (not a bare unmount), and the redo + /// restores it under the same id. + #[test] + fn create_text_footage_mounts_under_root_and_undoes() { + let _g = test_lock(); + oak_undo::global::clear().unwrap(); + let project = create_project(); + let before = oak_undo::global::count().unwrap(); + let text = create_text_footage_node(&project).expect("create text footage"); + assert_eq!( + oak_undo::global::count().unwrap(), + before + 1, + "the whole creation is one undo row" + ); + let root = { + let g = lock(&project); + assert_eq!(node_type_id(&g.graph, text), TEXT_FOOTAGE_TYPE_ID); + assert_eq!(node_label(&g.graph, text), TEXT_FOOTAGE_LABEL); + assert_eq!( + g.graph.get(text).map(|e| e.core.bin_folder), + Some(Some(g.root)), + "the text node names the root as its bin folder" + ); + g.root + }; + assert!( + root_children(&project).contains(&text), + "the text entry mounts under the root folder" + ); + + oak_undo::global::undo().unwrap(); + assert!( + !root_children(&project).contains(&text), + "the undo unmounts the entry" + ); + assert!( + !lock(&project).graph.is_valid(text), + "the undo removes the node itself — no orphan outlives it" + ); + + oak_undo::global::redo().unwrap(); + assert!( + root_children(&project).contains(&text), + "the redo remounts the entry" + ); + let g = lock(&project); + assert_eq!( + node_type_id(&g.graph, text), + TEXT_FOOTAGE_TYPE_ID, + "the restored node keeps its id and type" + ); + assert_eq!( + g.graph.get(text).map(|e| e.core.bin_folder), + Some(Some(root)), + "the restored node names the root again" + ); + drop(g); + oak_undo::global::clear().unwrap(); + } + + /// The text drop places a GENERATOR clip: the block spans the dropped + /// range on the target track and its `tex_in` reads the text node + /// instead of a decode. Non-text entries and out-of-range tracks are + /// rejected. + #[test] + fn place_text_clip_wires_the_generator_and_spans_the_range() { + let _g = test_lock(); + oak_undo::global::clear().unwrap(); + let project = create_project(); + let seq = create_sequence(&project, "Text Drop"); + let text = create_text_footage_node(&project).expect("create text footage"); + let (tb, track) = { + let g = lock(&project); + let tb = sequence_time_base(&g.graph, seq).expect("time base"); + let track = track_ids(&g.graph, seq, TrackType::Video)[0]; + (tb, track) + }; + let clip = place_text_clip(&project, seq, text, 0, 12, 137).expect("place the text clip"); + { + let g = lock(&project); + let (in_r, out_r, _) = clip_range(&g.graph, clip).expect("clip range"); + assert_eq!(in_r, ts_to_rational(12, tb)); + assert_eq!( + out_r, + ts_to_rational(137, tb), + "the clip spans the dropped frames" + ); + assert_eq!( + g.graph + .connected_output(clip, oak_node::block::clip_input::TEXTURE_INPUT, -1), + Some(text), + "the clip reads the generator through tex_in" + ); + assert_eq!( + clip_track(&g.graph, clip), + Some(track), + "the clip lands on the target video track" + ); + } + // Only the text generator takes the generator-clip path: a folder + // entry on the same call is rejected. + let folder = create_folder(&project, "Not Text").expect("create folder"); + assert!( + place_text_clip(&project, seq, folder, 0, 0, 10).is_err(), + "a non-text node is not placeable as a text clip" + ); + assert!( + place_text_clip(&project, seq, text, 9, 0, 10).is_err(), + "a track index past the video tracks is rejected" + ); + oak_undo::global::clear().unwrap(); + } + + // ---- default transitions (Ctrl+Shift+D / clip menu) --------------------- + + /// Two touching clips on the video track of a fresh project, plus the + /// track, the time base and the symmetric half length the transition + /// tests use. + fn two_touching_clips( + media: &std::path::Path, + ) -> (ProjectRef, NodeId, NodeId, NodeId, NodeId, (i64, i64), Rational) { + let project = create_project(); + let seq = create_sequence(&project, "Default Transition"); + let footage = import_footage(&project, media).expect("import"); + let a = place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0) + .expect("place a"); + let b = place_footage_clip(&project, seq, footage, TrackType::Video, 0, 10, 20, 0) + .expect("place b"); + let (tb, track) = { + let g = lock(&project); + ( + sequence_time_base(&g.graph, seq).expect("time base"), + track_ids(&g.graph, seq, TrackType::Video)[0], + ) + }; + let half = ts_to_rational(3, tb); + (project, seq, track, a, b, tb, half) + } + + /// 编辑 → 设为默认转场: one transition per contiguous seam around the + /// selection — the single seam here, built once even though both clips + /// are selected — wired onto both clips as ONE undo row. + #[test] + fn default_transition_covers_the_selected_seams() { + let _g = test_lock(); + oak_undo::global::clear().unwrap(); + let media = std::env::temp_dir() + .join(format!("oak_default_transition_{}.mp4", std::process::id())); + oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate"); + let (project, seq, track, a, b, tb, half) = two_touching_clips(&media); + + let before = oak_undo::global::count().unwrap(); + let created = add_default_transition(&project, seq, &[a, b], half).expect("add transition"); + assert_eq!( + created, 1, + "both clips name the same seam; it is built once" + ); + assert_eq!( + oak_undo::global::count().unwrap(), + before + 1, + "the whole batch is one undo row" + ); + + let transition = { + let g = lock(&project); + let blocks = track_behavior(&g.graph, track) + .map(|t| t.blocks.clone()) + .unwrap_or_default(); + assert_eq!(blocks.len(), 3, "the transition joins the track: {blocks:?}"); + let t = blocks[1]; + let seam = ts_to_rational(10, tb); + let core = block_core_of(&g.graph, t).expect("transition core"); + assert_eq!( + (core.in_(), core.out()), + (seam - half, seam + half), + "the transition straddles the seam" + ); + let behavior = g + .graph + .get(t) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .expect("transition behavior"); + assert_eq!( + (behavior.in_offset, behavior.out_offset), + (half, half), + "a default transition is symmetric" + ); + assert_eq!( + g.graph.connected_output(t, oak_node::block::transition_input::OUT_BLOCK, -1), + Some(a), + "the previous clip feeds out_block_in" + ); + assert_eq!( + g.graph.connected_output(t, oak_node::block::transition_input::IN_BLOCK, -1), + Some(b), + "the next clip feeds in_block_in" + ); + // The wedges the widget paints: the earlier clip's tail is the + // transition's in_offset, the later clip's head its out_offset. + let widths = clip_transition_widths(&g.graph, track); + assert_eq!( + widths.get(&a), + Some(&(None, Some(half))), + "the head clip's tail wedge" + ); + assert_eq!( + widths.get(&b), + Some(&(Some(half), None)), + "the tail clip's head wedge" + ); + assert_eq!(transition_of_clip(&g.graph, b, true), Some(t)); + assert_eq!(transition_of_clip(&g.graph, a, false), Some(t)); + assert_eq!( + transition_of_clip(&g.graph, a, true), + None, + "the first clip has nothing before it" + ); + assert_eq!( + transition_of_clip(&g.graph, b, false), + None, + "the last clip has nothing after it" + ); + t + }; + + let post = snapshot(&project, seq); + cycle_assert(&project, seq, &post, "add default transition"); + + // The seam carries a transition now: nothing left to build, and the + // refused batch leaves no empty undo row behind. + let rows = oak_undo::global::count().unwrap(); + assert!( + add_default_transition(&project, seq, &[a, b], half).is_err(), + "a seam that already carries a transition is skipped" + ); + assert_eq!(oak_undo::global::count().unwrap(), rows); + { + let g = lock(&project); + assert!(g.graph.is_valid(transition), "the refused batch changed nothing"); + } + + oak_undo::global::clear().unwrap(); + let _ = std::fs::remove_file(&media); + } + + /// The wedge drag (`TransitionChanged`): the later clip's head wedge + /// resizes the transition's out_offset, the earlier clip's tail wedge its + /// in_offset, the seam stays put, and a too-long request is clamped to + /// the dragged clip's length. + #[test] + fn transition_length_drag_moves_one_wedge_and_keeps_the_seam() { + let _g = test_lock(); + oak_undo::global::clear().unwrap(); + let media = + std::env::temp_dir().join(format!("oak_transition_drag_{}.mp4", std::process::id())); + oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate"); + let (project, seq, track, a, b, tb, half) = two_touching_clips(&media); + add_default_transition(&project, seq, &[a, b], half).expect("add transition"); + + let frame = ts_to_rational(1, tb); + let seam = ts_to_rational(10, tb); + let offsets = |p: &ProjectRef, t: NodeId| { + let g = lock(p); + g.graph + .get(t) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .map(|t| (t.in_offset, t.out_offset)) + .expect("transition behavior") + }; + let transition = { + let g = lock(&project); + transition_of_clip(&g.graph, b, true).expect("the head transition of b") + }; + + // Growing the head wedge of the later clip (edge = Start) only + // touches the out offset. + let longer = ts_to_rational(5, tb); + let applied = set_transition_length(&project, b, true, frame, longer).expect("grow"); + assert_eq!(applied, longer); + assert_eq!(offsets(&project, transition), (half, longer)); + { + let g = lock(&project); + let core = block_core_of(&g.graph, transition).expect("transition core"); + assert_eq!((core.in_(), core.out()), (seam - half, seam + longer)); + assert_eq!( + clip_range(&g.graph, a).map(|r| r.1), + Some(seam), + "the seam does not move" + ); + } + let post = snapshot(&project, seq); + cycle_assert(&project, seq, &post, "transition length"); + + // A request longer than the dragged clip is clamped to its length. + let applied = + set_transition_length(&project, b, true, frame, ts_to_rational(9_999, tb)).expect("clamp"); + assert_eq!( + applied, + ts_to_rational(10, tb), + "clamped to the later clip's length" + ); + + // The other side: the earlier clip's tail wedge (edge = End) drives + // the in offset. + let applied = set_transition_length(&project, a, false, frame, ts_to_rational(6, tb)) + .expect("grow in"); + assert_eq!(applied, ts_to_rational(6, tb)); + assert_eq!( + offsets(&project, transition), + (ts_to_rational(6, tb), ts_to_rational(10, tb)) + ); + let post = snapshot(&project, seq); + cycle_assert(&project, seq, &post, "transition length (in side)"); + + // A clip without a transition on that edge is rejected, not silently + // rewired. + assert!( + set_transition_length(&project, b, false, frame, half).is_err(), + "the last clip has no tail transition" + ); + assert!( + set_transition_length(&project, a, true, frame, half).is_err(), + "the first clip has no head transition" + ); + + oak_undo::global::clear().unwrap(); + let _ = std::fs::remove_file(&media); + } } diff --git a/crates/oak-app/src/oakui/mock.rs b/crates/oak-app/src/oakui/mock.rs index 441cadc54..7ac8e7286 100644 --- a/crates/oak-app/src/oakui/mock.rs +++ b/crates/oak-app/src/oakui/mock.rs @@ -516,6 +516,14 @@ pub struct MockEngine { /// the applied (id, track kind, track index, start frame) — drives /// app-level tests of the explorer→timeline drag). footage_drops: Vec, + /// Text-generator bin entries created via + /// [`AppEngine::create_text_footage`] since creation, as entry ids (mock + /// state; the mock's [`ProjectDataSource::roots`] lists them beside the + /// demo folders and files — drives app-level tests of the "添加文本素材" + /// action). + text_footages: Vec, + /// Id allocator for the text entries above. + next_text_footage_id: u64, /// The fake project library the project manager browses (M13 D4): an /// in-memory row set the library trait methods operate on, so the app /// flow (list / open / create / rename / duplicate / delete / import / /// export) is testable without a database. @@ -811,6 +819,8 @@ impl MockEngine { cpu_frame_cache: Mutex::new(HashMap::new()), imported_footage: Vec::new(), footage_drops: Vec::new(), + text_footages: Vec::new(), + next_text_footage_id: 100, library: demo_library(), next_library_id: 100, library_opened: Vec::new(), @@ -1347,6 +1357,49 @@ impl AppEngine for MockEngine { cx.notify(); } + fn add_adjustment_layer( + &mut self, + index: usize, + frame: Frame, + cx: &mut Context, + ) -> Result<(), String> { + let Some(kind) = self.tracks.get(index).map(|t| t.kind) else { + return Err(format!("no track at index {index}")); + }; + if kind != TrackKind::Video { + return Err("adjustment layers are only supported on video tracks".to_string()); + } + // The demo's canned layer: five seconds from the clicked frame, + // colored apart from the demo clips so the panel tests can tell it + // apart at a glance. + let fps = self.frame_rate(); + let length = (5.0 * fps.num as f64 / fps.den.max(1) as f64) + .round() + .max(1.0) as i64; + let start = Frame(frame.0.max(0)); + let clip = MockClip { + id: ClipId(self.next_mock_clip_id()), + range: FrameRange::new(start, Frame(start.0 + length)), + media_in: Frame(0), + label: "调整图层".into(), + color: Hsla { + h: 0.55, + s: 0.55, + l: 0.5, + a: 1.0, + }, + }; + let track = &mut self.tracks[index]; + let position = track + .clips + .iter() + .position(|c| c.range.start.0 > start.0) + .unwrap_or(track.clips.len()); + track.clips.insert(position, clip); + cx.notify(); + Ok(()) + } + fn set_track_height(&mut self, height: Pixels, cx: &mut Context) { self.set_track_height(height, cx); } @@ -1405,17 +1458,163 @@ impl AppEngine for MockEngine { } fn effect_params(&self, effect: EffectId) -> Option> { - // A canned text parameter behind a sentinel id, for the params - // view's text-field tests. + // A canned parameter list behind a sentinel id, mirroring the shape + // of the v3 text node's inputs (ids, value types, combo options and + // min bounds included) so the params view's tests can cover every + // control the text inspector builds. Three entries carry the HIDDEN + // flag: a real node's hidden inputs are dropped by the facade's + // `effect_params` before they get here, and the view must skip them + // on its own too (plugin and mock engines can hand them over). (effect.0 == 900).then(|| { - vec![crate::oakui::engine::EffectParam { - input_id: "text_in".to_string(), - display_name: "Text".to_string(), - value_type: oak_node::value::ValueType::Text, - value: oak_node::value::NodeValue::Text("

engine text

".to_string()), - flags: 0, - properties: Vec::new(), - }] + use oak_node::input::flags::{ARRAY, HIDDEN}; + use oak_node::value::{NodeValue, ValueType}; + + fn param( + input_id: &str, + display_name: &str, + value_type: ValueType, + value: NodeValue, + flags: u32, + properties: Vec<(&str, NodeValue)>, + ) -> crate::oakui::engine::EffectParam { + crate::oakui::engine::EffectParam { + input_id: input_id.to_string(), + display_name: display_name.to_string(), + value_type, + value, + flags, + properties: properties + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(), + } + } + + vec![ + param( + "pos_in", + "Position", + ValueType::Vec2, + NodeValue::Vec2([0.0, 0.0]), + 0, + Vec::new(), + ), + param( + "size_in", + "Size", + ValueType::Vec2, + NodeValue::Vec2([400.0, 300.0]), + 0, + vec![("min", NodeValue::Vec2([0.0, 0.0]))], + ), + param( + "plain_text_in", + "Text", + ValueType::Text, + NodeValue::Text("文本\nsecond line".to_string()), + 0, + vec![("multiline", NodeValue::Boolean(true))], + ), + param( + "text_in", + "Legacy text", + ValueType::Text, + NodeValue::Text("

engine text

".to_string()), + HIDDEN, + vec![("vieweronly", NodeValue::Boolean(true))], + ), + param( + "font_family_in", + "Font family", + ValueType::StrCombo, + NodeValue::StrCombo(String::new()), + 0, + vec![ + ("combo_option", NodeValue::Text("Sans".to_string())), + ("combo_option", NodeValue::Text("Serif".to_string())), + ], + ), + param( + "font_size_in", + "Font size", + ValueType::Float, + NodeValue::Float(72.0), + 0, + vec![("min", NodeValue::Float(1.0))], + ), + param( + "outline_enabled_in", + "Outline", + ValueType::Boolean, + NodeValue::Boolean(false), + 0, + Vec::new(), + ), + param( + "outline_color_in", + "Outline color", + ValueType::Color, + NodeValue::Color([0.0, 0.0, 0.0, 1.0]), + 0, + Vec::new(), + ), + param( + "outline_width_in", + "Outline width", + ValueType::Float, + NodeValue::Float(2.0), + 0, + vec![("min", NodeValue::Float(0.0))], + ), + param( + "glow_enabled_in", + "Glow", + ValueType::Boolean, + NodeValue::Boolean(false), + 0, + Vec::new(), + ), + param( + "glow_color_in", + "Glow color", + ValueType::Color, + NodeValue::Color([1.0, 1.0, 0.0, 1.0]), + 0, + Vec::new(), + ), + param( + "glow_radius_in", + "Glow radius", + ValueType::Float, + NodeValue::Float(8.0), + 0, + vec![("min", NodeValue::Float(0.0))], + ), + param( + "valign_in", + "Vertical align", + ValueType::Combo, + NodeValue::Combo(0), + HIDDEN, + Vec::new(), + ), + param( + "use_args_in", + "Use args", + ValueType::Boolean, + NodeValue::Boolean(true), + HIDDEN, + Vec::new(), + ), + param( + "args_in", + "Args", + ValueType::Text, + NodeValue::Text(String::new()), + ARRAY, + vec![("arraystart", NodeValue::Int(1))], + ), + ] }) } @@ -1808,6 +2007,14 @@ impl AppEngine for MockEngine { Ok(()) } + fn create_text_footage(&mut self, cx: &mut Context) -> Result { + let id = self.next_text_footage_id; + self.next_text_footage_id += 1; + self.text_footages.push(id); + cx.notify(); + Ok(id) + } + fn drop_footage( &mut self, id: u64, @@ -2351,6 +2558,16 @@ impl AppEngine for MockEngine { cx.notify(); } + fn add_default_transition( + &mut self, + _clips: Vec, + _cx: &mut Context, + ) -> Result { + // The demo timeline is not backed by a module graph, so there is no + // block to hang a transition on. + Err("default transition: the demo engine has no project".to_string()) + } + fn multicam_switch_to(&mut self, source: i32, split_clip: bool, cx: &mut Context) { let guard = self.ensure_demo_multicam(); let Some(demo) = guard.as_ref() else { @@ -2559,12 +2776,19 @@ impl NodeGraphDataSource for MockEngine { impl ProjectDataSource for MockEngine { fn roots(&self) -> Vec { - vec![ + let mut roots = vec![ ProjectEntry::new(1, crate::i18n::tr("bin.footage"), true), ProjectEntry::new(2, crate::i18n::tr("bin.music"), true), ProjectEntry::new(3, "第一稿.mp4", false), ProjectEntry::new(4, "aaa.ove", false), - ] + ]; + // Text generators created through the "添加文本素材" action (the + // shared node label `文本`, so the demo's browser stays locale- + // independent). + for &id in &self.text_footages { + roots.push(ProjectEntry::new(id, super::graphops::TEXT_FOOTAGE_LABEL, false)); + } + roots } fn children(&self, parent_id: u64) -> Vec { @@ -2805,6 +3029,13 @@ impl MockEngine { &self.footage_drops } + /// The text-generator entries created via + /// [`AppEngine::create_text_footage`] so far (mock state; the entry ids + /// are also listed by [`ProjectDataSource::roots`]). + pub fn text_footages(&self) -> &[u64] { + &self.text_footages + } + /// The display name of the project-explorer entry with `id`, if any. fn footage_entry_name(&self, id: u64) -> Option { for entry in self.roots() { @@ -3583,4 +3814,35 @@ mod tests { assert_eq!(engine.read(app).workarea(), Some((Frame(30), Frame(90)))); }); } + + /// 添加文本素材 on the mock: every call gets a fresh entry id, the ids + /// are listed by the bin as leaves labelled with the shared `文本` + /// node label (the drop path resolves them by name). + #[gpui::test] + async fn create_text_footage_lists_a_text_entry(cx: &mut TestAppContext) { + cx.update(|app| { + let engine = demo_engine(app); + let first = engine.update(app, |engine, cx| { + engine.create_text_footage(cx).expect("create text footage") + }); + let second = engine.update(app, |engine, cx| { + engine.create_text_footage(cx).expect("create text footage") + }); + assert_ne!(first, second, "each action gets a fresh entry id"); + assert_eq!(engine.read(app).text_footages(), &[first, second]); + let listed: Vec<_> = engine + .read(app) + .roots() + .into_iter() + .filter(|e| e.id == first || e.id == second) + .collect(); + assert_eq!(listed.len(), 2, "both text entries are listed in the bin"); + assert!( + listed.iter().all(|e| { + e.name == crate::oakui::graphops::TEXT_FOOTAGE_LABEL && !e.is_dir + }), + "the entries carry the shared 文本 label as leaves" + ); + }); + } } diff --git a/crates/oak-app/src/oakui/real.rs b/crates/oak-app/src/oakui/real.rs index 98258d1dd..7fa66a4e9 100644 --- a/crates/oak-app/src/oakui/real.rs +++ b/crates/oak-app/src/oakui/real.rs @@ -868,6 +868,12 @@ pub struct RealClip { /// The clip's texture chain contains a multicam node (the timeline /// overlay + the panel's detection read the same truth). multicam: bool, + /// Duration in frames of the transition attached to the clip's head + /// (the seam shared with the previous clip), if any. + in_transition: Option, + /// Duration in frames of the transition attached to the clip's tail + /// (the seam shared with the next clip), if any. + out_transition: Option, } impl ClipData for RealClip { @@ -894,6 +900,14 @@ impl ClipData for RealClip { fn is_multicam(&self) -> bool { self.multicam } + + fn in_transition(&self) -> Option { + self.in_transition + } + + fn out_transition(&self) -> Option { + self.out_transition + } } /// A running proxy transcode the engine drains on the tick loop (the @@ -1018,6 +1032,20 @@ impl TrackData for RealTrack { } } +/// The color of an adjustment layer's timeline band +/// ([`RealClip::color`]): a fixed slate that sits outside the per-clip +/// palette, so a band that carries no media is recognizable at a glance. +const ADJUSTMENT_LAYER_COLOR: Hsla = Hsla { + h: 0.72, + s: 0.3, + l: 0.58, + a: 1.0, +}; + +/// The default label of an adjustment layer (the block itself stores no +/// label; the snapshot synthesizes one like it does for unnamed clips). +const ADJUSTMENT_LAYER_LABEL: &str = "调整图层"; + /// A deterministic clip color from a stable per-clip index (the module /// graph exposes no clip color). fn clip_color(index: u64) -> Hsla { @@ -1528,6 +1556,10 @@ impl RealEngine { /// Builds an engine with no project open. pub fn new(cx: &mut Context) -> Self { + // Text nodes reach their layout/rasterizer through the process-global + // hooks of `oak_node::nodes::textbackend` (oak-node links no font or + // shaping crate); fill them in before anything can run a node. + super::textengine::install(); let rate = VideoFormat::hd_1080p25().rate; let (full_res_tx, full_res_rx) = mpsc::channel::(); let (thumb_tx, thumb_rx) = mpsc::channel::(); @@ -3419,6 +3451,9 @@ impl RealEngine { let height = graphops::track_behavior(graph, track) .map(|t| px(oak_node::track::internal_height_to_pixel_height(t.height).max(24) as f32)) .unwrap_or(px(64.0)); + // The transition wedges the track's clips carry (left = head + // transition, right = tail transition), keyed by clip block. + let widths = graphops::clip_transition_widths(graph, track); let clips = graphops::clip_ids(graph, track) .iter() .enumerate() @@ -3427,17 +3462,30 @@ impl RealEngine { let to_ts = |r: oak_core::Rational| { tb.map(|tb| graphops::rational_to_ts(r, tb)).unwrap_or(0) }; + let wedge = |w: Option| w.map(|r| Frame(to_ts(r))); + let (head, tail) = widths.get(&block).copied().unwrap_or((None, None)); + let entry = graph.get(block)?; + let is_adjustment = entry + .behavior + .as_any() + .map(|a| a.is::()) + .unwrap_or(false); // The clip's color is locked in at creation time (its // `override_color`); clips without one (older projects) // fall back to the track-relative palette so their color - // still varies. - let entry = graph.get(block)?; - let color = if entry.core.override_color >= 0 { + // still varies. Adjustment layers wear their own fixed + // color instead of a palette entry: their band carries no + // media and must read as an effect layer, not a clip. + let color = if is_adjustment { + ADJUSTMENT_LAYER_COLOR + } else if entry.core.override_color >= 0 { clip_color(entry.core.override_color as u64) } else { clip_color(clip_index as u64) }; - let label = if entry.core.label.is_empty() { + let label = if is_adjustment && entry.core.label.is_empty() { + ADJUSTMENT_LAYER_LABEL.to_string() + } else if entry.core.label.is_empty() { format!("Clip {}", clip_index + 1) } else { entry.core.label.clone() @@ -3452,6 +3500,8 @@ impl RealEngine { color, block, multicam, + in_transition: wedge(head), + out_transition: wedge(tail), }) }) .collect(); @@ -3518,7 +3568,7 @@ impl RealEngine { let block = graphops::id_of(id.0)?; let project = self.project_ref()?; let guard = graphops::lock(project); - graphops::clip_behavior(&guard.graph, block)?; + graphops::block_core_of(&guard.graph, block)?; // The clip must be on the CURRENT timeline (a stale id of a // removed clip must not resolve). self.tracks @@ -3559,7 +3609,7 @@ impl RealEngine { }; let guard = graphops::lock(project); // A clip block node on the current timeline: its stack is the target. - if graphops::clip_behavior(&guard.graph, node).is_some() + if graphops::block_core_of(&guard.graph, node).is_some() && self .tracks .iter() @@ -3574,7 +3624,7 @@ impl RealEngine { let Some(block) = graphops::id_of(clip.id.0) else { continue; }; - if graphops::clip_behavior(&guard.graph, block).is_none() { + if graphops::block_core_of(&guard.graph, block).is_none() { continue; } if let Some(effect) = super::effectchain::chain(&guard.graph, block) @@ -4194,6 +4244,34 @@ impl AppEngine for RealEngine { ); } + fn add_adjustment_layer( + &mut self, + index: usize, + frame: Frame, + cx: &mut Context, + ) -> Result<(), String> { + let Some(project) = self.project.clone() else { + return Err("add adjustment layer: no project is open".to_string()); + }; + let Some(seq) = self.sequence else { + return Err("add adjustment layer: no sequence is open".to_string()); + }; + let Some(tb) = self.time_base() else { + return Err("add adjustment layer: the sequence has no frame rate".to_string()); + }; + let Some(track) = self.tracks.get(index).map(|t| t.track) else { + return Err(format!("add adjustment layer: no track at index {index}")); + }; + // The default span: five seconds from the clicked frame (the + // snapshot's non-snapping approximation of the C++ default). + let fps = tb.1 as f64 / tb.0.max(1) as f64; + let length = (5.0 * fps).round().max(1.0) as i64; + let in_ts = frame.0.max(0); + let result = graphops::create_adjustment_layer(&project, seq, track, in_ts, in_ts + length); + self.apply_edit(result.clone().map(|_| ()), "add adjustment layer", cx); + result.map(|_| ()) + } + fn set_track_height(&mut self, height: Pixels, cx: &mut Context) { let (Some(project), Some(seq)) = (self.project.clone(), self.sequence) else { return; @@ -4676,11 +4754,40 @@ impl AppEngine for RealEngine { } cx.notify(); } - // Selection / zoom / transition / track-selected / context-menu: + TimelineEvent::TransitionChanged { + clip, + edge, + new_length, + } => { + let Some(block) = self.clip_block(*clip) else { + return; + }; + if self.clip_track_locked(block) { + return; + } + let Some(project) = self.project.clone() else { + return; + }; + let Some(tb) = self.time_base() else { + return; + }; + // The widget works in the sequence's frame unit, the + // command layer in seconds. + let frame = graphops::ts_to_rational(1, tb); + let requested = graphops::ts_to_rational(new_length.0.max(1), tb); + let result = graphops::set_transition_length( + &project, + block, + matches!(edge, TrimEdge::Start), + frame, + requested, + ); + self.apply_edit(result.map(|_| ()), "transition length", cx); + } + // Selection / zoom / track-selected / context-menu: // not editable (the right-click is answered by the panel's popup). TimelineEvent::SelectionChanged | TimelineEvent::TrackSelected { .. } - | TimelineEvent::TransitionChanged { .. } | TimelineEvent::ContextMenuRequested { .. } | TimelineEvent::ZoomChanged(_) => {} TimelineEvent::TrackToggleRequested { track, toggle } => { @@ -5147,14 +5254,24 @@ impl AppEngine for RealEngine { // sequence node (TEXTURE_INPUT, like `clip_connected_sequence` // resolves), so a multicam source sequence drops as a single // multi-cam clip the panel detects. - let is_sequence = { + // + // Text entry drop: a GENERATOR clip — the clip reads the text + // node's output through `tex_in` instead of decoding media. + let (is_sequence, is_text) = { let guard = graphops::lock(&project); - graphops::sequence_behavior(&guard.graph, footage).is_some() + ( + graphops::sequence_behavior(&guard.graph, footage).is_some(), + graphops::node_type_id(&guard.graph, footage) == graphops::TEXT_FOOTAGE_TYPE_ID, + ) }; if is_sequence { self.drop_sequence_entry(&project, footage, track_index, time, cx); return; } + if is_text { + self.drop_text_entry(&project, footage, track_index, time, cx); + return; + } let (filename, video_streams, total_streams, seconds) = { let guard = graphops::lock(&project); let Some(f) = graphops::footage_behavior(&guard.graph, footage) else { @@ -5738,6 +5855,15 @@ impl AppEngine for RealEngine { Ok(id.identity()) } + fn create_text_footage(&mut self, cx: &mut Context) -> Result { + let Some(project) = self.project.clone() else { + return Err(crate::i18n::tr("seqprops.error.no_project").to_string()); + }; + let id = graphops::create_text_footage_node(&project)?; + self.apply_edit(Ok(()), "add text footage", cx); + Ok(id.identity()) + } + fn update_sequence_parameters( &mut self, id: u64, @@ -6427,6 +6553,45 @@ impl AppEngine for RealEngine { self.apply_edit(result, "multicam enable/disable", cx); } + fn add_default_transition( + &mut self, + clips: Vec, + cx: &mut Context, + ) -> Result { + let Some(project) = self.project.clone() else { + return Err("default transition: no project is open".to_string()); + }; + let Some(seq) = self.sequence else { + return Err("default transition: no sequence is open".to_string()); + }; + let Some(tb) = self.time_base() else { + return Err("default transition: the sequence has no frame rate".to_string()); + }; + // The preference is the WHOLE transition length; a default + // transition is symmetric, so each wedge takes half of it (at least + // one frame — `ts_to_rational` needs a positive frame count). + let seconds: f64 = config_get_string(CONFIG_KEY_DEFAULT_TRANSITION_SEC) + .parse() + .unwrap_or_else(|_| { + DEFAULT_TRANSITION_SEC + .parse() + .expect("the compiled-in default parses") + }); + let fps = tb.1 as f64 / tb.0.max(1) as f64; + let half_frames = (seconds * fps / 2.0).round().max(1.0) as i64; + let half = graphops::ts_to_rational(half_frames, tb); + let blocks: Vec = clips + .iter() + .filter_map(|id| { + let block = self.clip_block(*id)?; + (!self.clip_track_locked(block)).then_some(block) + }) + .collect(); + let result = graphops::add_default_transition(&project, seq, &blocks, half); + self.apply_edit(result.clone().map(|_| ()), "add default transition", cx); + result + } + fn multicam_switch_to(&mut self, source: i32, split_clip: bool, cx: &mut Context) { let Some(project) = self.project.clone() else { return; @@ -6726,6 +6891,64 @@ impl RealEngine { Ok(audio_clip) } + /// Drops a text-project entry on the timeline as a GENERATOR clip: the + /// clip spans five seconds from the drop point and its `tex_in` reads + /// the text node, so the generator re-renders at whatever time the + /// clip spans (no media to decode and no footage length to clamp to). + /// The target track policy is the sequence drop's: the pointed display + /// row when it is a video track, else the first video track. Undoable + /// as one "Add Clip". + fn drop_text_entry( + &mut self, + project: &ProjectRef, + text: NodeId, + track_index: usize, + time: Frame, + cx: &mut Context, + ) { + let Some(host_seq) = self.sequence else { + println!("[real engine] drop text: no sequence open"); + return; + }; + let video_target = if self + .tracks + .get(track_index) + .is_some_and(|t| t.kind == TrackKind::Video) + { + track_index + } else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video) + { + index + } else { + println!("[real engine] drop text: no video track"); + return; + }; + let video_index = self.tracks[video_target].track_index; + // Five seconds of the host sequence's frames (the drop's default + // extent — the text clip has no media length to take one from). + let (in_ts, length_ts) = { + let guard = graphops::lock(project); + let Ok(tb) = graphops::sequence_time_base(&guard.graph, host_seq) + .ok_or_else(|| "host sequence has no frame rate".to_string()) + else { + println!("[real engine] drop text: host has no frame rate"); + return; + }; + let fps_f = tb.1.max(1) as f64 / tb.0.max(1) as f64; + ((time.0.max(0)), ((5.0 * fps_f).round() as i64).max(1)) + }; + let placed = graphops::place_text_clip( + project, + host_seq, + text, + video_index, + in_ts, + in_ts + length_ts, + ); + let result = placed.map(|_| ()); + self.apply_edit(result, "drop text", cx); + } + /// Drops a sequence-project entry on the timeline as a NESTED-SEQUENCE /// clip: ONE video clip on the video track (not the sequence's tracks — /// a frame, not a folder, of tracks), fed from the sequence node. This @@ -10258,6 +10481,315 @@ mod tests { ); } + /// 添加文本素材 through the engine facade: `create_text_footage` + /// creates a `text3` generator labelled "文本" mounted under the root + /// (the explorer lists it; it is NOT a sequence), and dropping it lands + /// a five-second generator clip on the host sequence's first video + /// track, wired through `tex_in` and undoable as one row. + #[gpui::test] + async fn engine_creates_text_footage_and_drops_it_on_the_timeline( + cx: &mut gpui::TestAppContext, + ) { + let _media = media_lock(); + let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); + + let text = cx.update(|app| { + engine + .update(app, |engine, cx| engine.create_text_footage(cx)) + .expect("create text footage") + }); + let project = cx.read(|app| engine.read(app).project.clone().expect("project")); + let node = graphops::id_of(text).expect("text node"); + { + let guard = graphops::lock(&project); + assert_eq!( + graphops::node_type_id(&guard.graph, node), + graphops::TEXT_FOOTAGE_TYPE_ID + ); + assert_eq!( + graphops::node_label(&guard.graph, node), + graphops::TEXT_FOOTAGE_LABEL + ); + let children = guard + .graph + .get(guard.root) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .map(|f| f.children.clone()) + .unwrap_or_default(); + assert!( + children.contains(&node), + "the text entry mounts under the root folder" + ); + } + let is_seq = cx.read(|app| engine.read(app).entry_is_sequence(text)); + assert!(!is_seq, "a text entry is not a sequence"); + + // Open a host sequence; the drop targets its first video track. + let seq = cx.update(|app| { + engine + .update(app, |engine, cx| { + engine + .create_sequence_with_params( + "Text Drop".to_string(), + VideoFormat { + width: 1920, + height: 1080, + rate: FrameRate::new(30000, 1001), + }, + false, + cx, + ) + .expect("create sequence") + }) + }); + cx.update(|app| { + engine.update(app, |engine, cx| { + engine.drop_footage(text, TrackKind::Video, 0, Frame(25), cx) + }) + }); + + let seq_node = graphops::id_of(seq).expect("sequence node"); + // The drop row is a display row: the engine's track view runs + // top-to-bottom, so row 0 is the topmost video track of the + // sequence's list (V2 with the default layout). + let target_row = cx.read(|app| { + let row = &engine.read(app).tracks[0]; + assert_eq!(row.kind, TrackKind::Video, "the drop row is a video track"); + row.track_index + }); + let (tb, tracks) = { + let guard = graphops::lock(&project); + ( + graphops::sequence_time_base(&guard.graph, seq_node).expect("time base"), + graphops::track_ids(&guard.graph, seq_node, TrackType::Video), + ) + }; + let track = tracks[target_row]; + { + let guard = graphops::lock(&project); + let clips = graphops::clip_ids(&guard.graph, track); + assert_eq!(clips.len(), 1, "the drop lands exactly one clip: {clips:?}"); + let clip = clips[0]; + assert_eq!( + graphops::clip_track(&guard.graph, clip), + Some(track), + "the clip lands on the drop row's video track" + ); + for (index, other) in tracks.iter().enumerate() { + if index == target_row { + continue; + } + assert!( + graphops::clip_ids(&guard.graph, *other).is_empty(), + "the other video tracks stay untouched" + ); + } + let (in_r, out_r, _) = graphops::clip_range(&guard.graph, clip).expect("clip range"); + let fps = tb.1 as f64 / tb.0 as f64; + let length = (5.0 * fps).round().max(1.0) as i64; + assert_eq!(in_r, graphops::ts_to_rational(25, tb)); + assert_eq!( + out_r, + graphops::ts_to_rational(25 + length, tb), + "the clip spans five seconds from the drop point" + ); + assert_eq!( + guard + .graph + .connected_output(clip, oak_node::block::clip_input::TEXTURE_INPUT, -1), + Some(node), + "the clip reads the generator through tex_in" + ); + } + + // One undo removes the clip (and its edge) again. + oak_undo::global::undo().unwrap(); + { + let guard = graphops::lock(&project); + assert!( + graphops::clip_ids(&guard.graph, track).is_empty(), + "the undo removes the dropped clip" + ); + assert!( + graphops::track_behavior(&guard.graph, track) + .map(|t| t.blocks.is_empty()) + .unwrap_or(false), + "the undo also drops the gap the placement inserted" + ); + } + oak_undo::global::clear().unwrap(); + } + + /// 调整图层 through the engine facade: `add_adjustment_layer` places a + /// five-second empty block at the clicked frame on a video track (an + /// `AdjustmentBlockBehavior` with no footage and no texture wiring), + /// refuses audio tracks, resolves through the timeline selection, and is + /// undone by one undo step. + #[gpui::test] + async fn engine_adds_and_undoes_adjustment_layer(cx: &mut gpui::TestAppContext) { + let _media = media_lock(); + let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); + + let seq = cx.update(|app| { + engine.update(app, |engine, cx| { + engine + .create_sequence_with_params( + "Adjustment Layer".to_string(), + VideoFormat { + width: 1920, + height: 1080, + rate: FrameRate::new(25, 1), + }, + false, + cx, + ) + .expect("create sequence") + }) + }); + let seq_node = graphops::id_of(seq).expect("sequence node"); + let project = cx.read(|app| engine.read(app).project.clone().expect("project")); + let tb = { + let guard = graphops::lock(&project); + graphops::sequence_time_base(&guard.graph, seq_node).expect("time base") + }; + let fps = tb.1 as f64 / tb.0.max(1) as f64; + let length = (5.0 * fps).round().max(1.0) as i64; + + // An audio row is not a legal host. + let audio_row = cx.read(|app| { + engine + .read(app) + .tracks + .iter() + .position(|row| row.kind == TrackKind::Audio) + .expect("the default layout has audio rows") + }); + let rejected = cx.update(|app| { + engine.update(app, |engine, cx| { + engine.add_adjustment_layer(audio_row, Frame(0), cx) + }) + }); + let error = rejected.expect_err("audio tracks reject adjustment layers"); + assert!( + error.contains("video track"), + "the rejection names the constraint: {error}" + ); + + // Row 0 is the topmost video row; frame 12 anchors the new layer. + let target_row = cx.read(|app| { + let row = &engine.read(app).tracks[0]; + assert_eq!(row.kind, TrackKind::Video, "row 0 is a video track"); + row.track_index + }); + cx.update(|app| { + engine.update(app, |engine, cx| { + engine.add_adjustment_layer(0, Frame(12), cx) + }) + }) + .expect("add adjustment layer"); + + // The snapshot carries the new layer: the fixed slate (outside the + // per-clip palette), the synthesized label, and the five-second span + // from the clicked frame. + let (track, clip) = cx.read(|app| { + let engine = engine.read(app); + let row = &engine.tracks[0]; + assert_eq!(row.clips.len(), 1, "the add lands exactly one block"); + (row.track, row.clips[0].clone()) + }); + assert_eq!(clip.range, FrameRange::new(Frame(12), Frame(12 + length))); + assert_eq!(clip.media_in, Frame(0)); + assert_eq!(clip.label, SharedString::from("调整图层")); + assert_eq!(clip.color, ADJUSTMENT_LAYER_COLOR); + assert!( + (0..16).all(|index| clip.color != clip_color(index)), + "the adjustment slate sits outside the per-clip palette" + ); + let block = clip.block; + + // The graph holds an adjustment block on the row's track, spanning + // the same range, with no texture source. + { + let guard = graphops::lock(&project); + let videos = graphops::track_ids(&guard.graph, seq_node, TrackType::Video); + assert_eq!( + videos[target_row], track, + "the display row maps to the graph track" + ); + assert_eq!(graphops::clip_track(&guard.graph, block), Some(track)); + assert_eq!( + graphops::clip_ids(&guard.graph, track), + vec![block], + "the placed block is the track's only clip" + ); + for (index, other) in videos.iter().enumerate() { + if index == target_row { + continue; + } + assert!( + graphops::clip_ids(&guard.graph, *other).is_empty(), + "the other video tracks stay untouched" + ); + } + let entry = guard.graph.get(block).expect("block node"); + assert!( + entry + .behavior + .as_any() + .map(|a| a.is::()) + .unwrap_or(false), + "the placed block carries the adjustment behavior" + ); + let (in_r, out_r, media_in) = + graphops::clip_range(&guard.graph, block).expect("clip range"); + assert_eq!(in_r, graphops::ts_to_rational(12, tb)); + assert_eq!( + out_r, + graphops::ts_to_rational(12 + length, tb), + "the layer spans five seconds from the clicked frame" + ); + assert_eq!(media_in, oak_core::Rational::new(0, 1)); + assert!( + !guard.graph.is_input_connected( + block, + oak_node::block::adjustment_input::TEXTURE_INPUT, + -1 + ), + "the adjustment layer carries no texture source" + ); + } + + // The timeline selection resolves the snapshot id to the block (the + // inspector targets adjustment layers like any clip). + cx.update(|app| { + engine.update(app, |engine, cx| { + engine.set_selected_clips(vec![ClipId(block.identity())], cx) + }) + }); + let selected = cx.read(|app| engine.read(app).selected_clip_node()); + assert_eq!(selected, Some(block), "the selected layer resolves to its block"); + + // One undo removes the placement (and the leading gap) again. + oak_undo::global::undo().unwrap(); + { + let guard = graphops::lock(&project); + assert!( + graphops::clip_ids(&guard.graph, track).is_empty(), + "the undo removes the placed adjustment layer" + ); + assert!( + graphops::track_behavior(&guard.graph, track) + .map(|t| t.blocks.is_empty()) + .unwrap_or(false), + "the undo also drops the gap the placement inserted" + ); + } + oak_undo::global::clear().unwrap(); + } + /// `update_sequence_parameters` rewrites the name, the format and the /// interlace flag in one call; `sequence_parameters` reads the new /// state back (the properties dialog's commit path). diff --git a/crates/oak-app/src/oakui/renderops.rs b/crates/oak-app/src/oakui/renderops.rs index 8a702702b..81f00f53e 100644 --- a/crates/oak-app/src/oakui/renderops.rs +++ b/crates/oak-app/src/oakui/renderops.rs @@ -35,7 +35,9 @@ use oak_node::id::NodeId; use oak_node::track::TrackType; use oak_render::manager::RenderManager; use oak_render::procpool::ShmFrameRef; -use oak_render::ticket::{AudioTicketParams, MontageClip, TicketPayload, VideoTicketParams}; +use oak_render::ticket::{ + AdjustmentSpan, AudioTicketParams, MontageClip, TicketPayload, VideoTicketParams, +}; use super::engine::{ExportEvent, ExportSession}; use super::frames::{bgra_bytes_to_render_image, f32_rgba_to_bgra_image}; @@ -240,17 +242,29 @@ fn clip_effects(g: &oak_node::graph::Graph, block_id: NodeId) -> Vec