From 4a2614b3fca615803b02076da423710d8f087b5f Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Thu, 10 Sep 2026 22:03:15 +0800 Subject: [PATCH] timeline: adjustment layers and first-class transitions Adjustment layers (docs/zh/plans/adjustment-layers-and-transitions.md): a new timeline block type whose effect chain grades the composite of every video track below it, over its own range (spanning clips or a slice of one). The graph path flushes the lower tracks at the block's track boundary and sweeps the composite through the chain via a transient texture-source node; the montage path mirrors it with AdjustmentSpan tickets (wire-compatible), so worker previews and exports agree. An empty-area context menu creates one; the block trims/moves/deletes like a clip, with undo everywhere. Transitions: seam blocks come alive - cross dissolve/fade/wipe/slide evaluate both neighbors through the graph path with progress from the transition's own range (never the whole clip). Ctrl+Shift+D or the clip menu inserts a default transition; the gpui wedges render and drag to resize offsets undoably, and TransitionRemoveCommand now restores offsets and edges on undo. The transitionfx node form runs the same shaders on an adjustment layer with progress_in auto-filled from the layer's span (explicit value wins). Also: every built-in effect name and parameter name is now translatable (360 node.* keys per locale, zh-CN fully translated, two coverage tests guard future gaps); the new nodes register in nodes/mod.rs with the factory smoke table updated; textfootage and adjustment-layer i18n keys included. --- assets/i18n/de.yaml | 376 +++++ assets/i18n/en-US.yaml | 376 +++++ assets/i18n/es.yaml | 376 +++++ assets/i18n/fr.yaml | 376 +++++ assets/i18n/ja.yaml | 376 +++++ assets/i18n/pt.yaml | 376 +++++ assets/i18n/ru.yaml | 376 +++++ assets/i18n/zh-CN.yaml | 376 +++++ crates/oak-app/src/app.rs | 57 + crates/oak-app/src/i18n.rs | 56 + crates/oak-app/src/oakui/effectchain.rs | 484 +++++- crates/oak-app/src/oakui/engine.rs | 35 + crates/oak-app/src/oakui/graphops.rs | 1323 ++++++++++++++++- crates/oak-app/src/oakui/mock.rs | 286 +++- crates/oak-app/src/oakui/real.rs | 554 ++++++- crates/oak-app/src/oakui/renderops.rs | 432 +++++- crates/oak-app/src/panels/commands.rs | 7 + crates/oak-app/src/panels/timeline.rs | 130 +- crates/oak-cli/src/engine.rs | 1 + crates/oak-node/src/block.rs | 212 ++- crates/oak-node/src/nodes/compositesource.rs | 184 +++ crates/oak-node/src/nodes/mod.rs | 52 +- crates/oak-node/src/nodes/transitionfx.rs | 766 ++++++++++ crates/oak-node/src/nodes/transitions.rs | 608 ++++++++ crates/oak-node/src/serializer.rs | 7 + crates/oak-node/tests/factory_smoke_test.rs | 25 + crates/oak-node/tests/serializer_test.rs | 46 +- crates/oak-render/examples/bench_playback.rs | 1 + crates/oak-render/examples/bench_process.rs | 1 + crates/oak-render/src/autocacher.rs | 2 + crates/oak-render/src/eval.rs | 844 +++++++++-- crates/oak-render/src/ipc.rs | 105 ++ crates/oak-render/src/procpool.rs | 5 + crates/oak-render/src/ticket.rs | 37 + crates/oak-render/src/worker.rs | 3 + crates/oak-render/tests/adjustment_layer.rs | 352 +++++ crates/oak-render/tests/ticket_worker_test.rs | 1 + crates/oak-render/tests/transition_render.rs | 397 +++++ crates/oak-render/tests/transitionfx.rs | 241 +++ crates/oak-task/src/nodeops.rs | 24 +- crates/oak-task/src/project/saveotio.rs | 29 +- crates/oak-task/src/render.rs | 2 + crates/oak-timeline/src/undogeneral.rs | 178 ++- crates/oak-timeline/src/util.rs | 88 +- crates/oak-timeline/tests/domain_test.rs | 230 ++- crates/oak-worker/src/worker.rs | 8 +- .../oak-worker/tests/procpool_integration.rs | 5 + .../adjustment-layers-and-transitions.md | 253 ++++ 48 files changed, 10837 insertions(+), 242 deletions(-) create mode 100644 crates/oak-node/src/nodes/compositesource.rs create mode 100644 crates/oak-node/src/nodes/transitionfx.rs create mode 100644 crates/oak-node/src/nodes/transitions.rs create mode 100644 crates/oak-render/tests/adjustment_layer.rs create mode 100644 crates/oak-render/tests/transition_render.rs create mode 100644 crates/oak-render/tests/transitionfx.rs create mode 100644 docs/zh/plans/adjustment-layers-and-transitions.md 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