mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
Freeform Pen: Alt-clicks draw straight segments; Cmd/Ctrl is Direct Selection with the new pens (#3085) (#3087)
* Tools: Freeform Pen and Curvature Pen (#1054) The Pen flyout (P / Shift+P) now holds Pen, Freeform Pen and Curvature Pen. Freeform Pen: a drag draws freehand; on release the trace is fitted with cubic Beziers within the options bar's Curve Fit (default 2 px) by photocraft_vector::trace::fit_trace (Schneider, "An Algorithm for Automatically Fitting Digitized Curves", Graphics Gems 1990, the fitting Make Work Path already uses), keeping turns sharper than 60 degrees as corners. Releasing on the start point closes the path. Curvature Pen: each click adds a smooth point and the path runs through every point (Catmull-Rom style handles from photocraft_vector::edit::curvature_knots); dragging a point moves it, double-clicking toggles corner/smooth, clicking the first point closes, Enter/Esc finish and Backspace removes the last point, as with the Pen. Both commit through the Pen's path: a shape layer in Shape mode, else the targeted vector mask, else the work path, as one History step. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Freeform Pen: Alt-clicks draw straight segments; Cmd/Ctrl is Direct Selection with the new pens (#3085) The Freeform Pen now draws through lasso_ui, as the Lasso does: Alt held at the release keeps the path open, each Alt-click adds a straight segment, releasing Alt with the button down goes on freehand, with it up finishes the path; clicking the start point closes it. freeform_finish fits each freehand run on its own and joins the runs with straight segments between corner anchors. Cmd/Ctrl with the Freeform or Curvature Pen is the temporary Direct Selection, as with the Pen (#790); it finishes a path in progress first. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3455234537
commit
f0d3156ad6
21 files changed
+235
-54
No files matched your search
@@ -2931,13 +2931,14 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect:
|
||||
}
|
||||
let lasso_retracted = response.secondary_clicked()
|
||||
&& match tool {
|
||||
Tool::Lasso => crate::lasso_ui::undo_last_vertex(app),
|
||||
Tool::Lasso | Tool::FreeformPen => crate::lasso_ui::undo_last_vertex(app),
|
||||
Tool::PolygonLasso => polygon_retract(app),
|
||||
_ => false,
|
||||
};
|
||||
// The Lasso reads its own presses and drops the moves of a drag it didn't start, so an
|
||||
// open Free Transform box takes the usual drag path instead (#2153).
|
||||
if tool == Tool::Lasso && !transform_owns_pointer {
|
||||
// The Lasso (and the Freeform Pen, which draws the same way) reads its own presses and
|
||||
// drops the moves of a drag it didn't start, so an open Free Transform box takes the
|
||||
// usual drag path instead (#2153).
|
||||
if crate::lasso_ui::owns(tool) && !transform_owns_pointer {
|
||||
crate::lasso_ui::canvas_input(app, &ctx, &xf, &response);
|
||||
(buttons.started, buttons.dragged, buttons.stopped, buttons.clicked) = (false, false, false, false);
|
||||
}
|
||||
@@ -4100,7 +4101,11 @@ fn draw_drag_preview(app: &mut PhotocraftApp, painter: &egui::Painter, xf: &View
|
||||
}
|
||||
}
|
||||
Tool::FreeformPen => {
|
||||
let pts: Vec<Pos2> = d.points.iter().map(|p| xf.to_screen(p[0] as f32, p[1] as f32)).collect();
|
||||
let mut pts: Vec<Pos2> = d.points.iter().map(|p| xf.to_screen(p[0] as f32, p[1] as f32)).collect();
|
||||
// Between ⌥-clicks: the straight segment the next click makes.
|
||||
if let Some(lasso) = &d.lasso {
|
||||
pts.push(xf.to_screen(lasso.cursor[0] as f32, lasso.cursor[1] as f32));
|
||||
}
|
||||
painter.add(egui::Shape::line(pts, Stroke::new(1.5, crate::theme::Tokens::get(painter.ctx()).accent)));
|
||||
}
|
||||
Tool::Lasso | Tool::Patch | Tool::ContentAwareMove => {
|
||||
@@ -4855,7 +4860,7 @@ pub(crate) fn finish_gesture(app: &mut PhotocraftApp, d: Drag) {
|
||||
}
|
||||
match d.tool {
|
||||
Tool::ObjectSelection => crate::retouch_ui::finish_object_selection(app, d.start, [end[0], end[1]], d.modifiers),
|
||||
Tool::FreeformPen => crate::vector_ui::freeform_finish(app, &d.points),
|
||||
Tool::FreeformPen => crate::vector_ui::freeform_finish(app, &d.points, d.lasso.as_ref().map_or(&[], |l| l.straight())),
|
||||
// Photoshop: a click without dragging opens Create Rectangle / Ellipse / … instead.
|
||||
t if crate::vector_ui::is_shape_tool(t)
|
||||
&& crate::shape_dialog::is_click(d.start, &d.points, app.current_zoom())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Direct Selection tool (A, #790), the Convert Point tool (#3204), and the Pen's ⌘/Ctrl (Direct
|
||||
//! Selection) and ⌥/Alt (Convert Point) modifiers, as in Photoshop. It edits the paths the canvas
|
||||
//! shows: the targeted vector mask or the active shape layer's path, and the work path.
|
||||
//! Direct Selection tool (A, #790), the Convert Point tool (#3204), the Pens' ⌘/Ctrl (Direct
|
||||
//! Selection: the Pen, Freeform Pen and Curvature Pen) and the Pen's ⌥/Alt (Convert Point)
|
||||
//! modifiers, as in Photoshop. It edits the paths the canvas shows: the targeted vector mask or
|
||||
//! the active shape layer's path, and the work path.
|
||||
//!
|
||||
//! * Click an anchor to select it (⇧ adds or removes, ⌥ selects its whole subpath), drag on empty
|
||||
//! canvas to marquee-select anchors, click empty canvas to deselect.
|
||||
@@ -106,10 +107,12 @@ impl Edit {
|
||||
}
|
||||
}
|
||||
|
||||
/// Is Direct Selection what the pointer does now: its own tool, or the Pen, an anchor point tool
|
||||
/// or Convert Point with ⌘/Ctrl held.
|
||||
/// Is Direct Selection what the pointer does now: its own tool, or a Pen (the Pen, Freeform Pen
|
||||
/// or Curvature Pen), an anchor point tool or Convert Point with ⌘/Ctrl held.
|
||||
fn direct(tool: Tool, mods: Modifiers) -> bool {
|
||||
tool == Tool::DirectSelection || (matches!(tool, Tool::Pen | Tool::AddAnchorPoint | Tool::DeleteAnchorPoint | Tool::ConvertPoint) && mods.command)
|
||||
tool == Tool::DirectSelection
|
||||
|| (matches!(tool, Tool::Pen | Tool::FreeformPen | Tool::CurvaturePen | Tool::AddAnchorPoint | Tool::DeleteAnchorPoint | Tool::ConvertPoint)
|
||||
&& mods.command)
|
||||
}
|
||||
|
||||
/// Is Convert Point what the pointer does now: its own tool, or the Pen with ⌥ held while no path
|
||||
@@ -173,10 +176,14 @@ pub fn pointer(app: &mut PhotocraftApp, ev: ToolEvent, mods: Modifiers) -> bool
|
||||
if !(direct(tool, mods) || convert) {
|
||||
return false;
|
||||
}
|
||||
if tool == Tool::Pen && !pen_idle {
|
||||
if !pen_idle && tool != Tool::DirectSelection {
|
||||
// ⌘-click ends the path being drawn, which becomes editable, as in Photoshop.
|
||||
crate::vector_ui::pen_commit(app, false);
|
||||
}
|
||||
if tool == Tool::FreeformPen {
|
||||
// The same for a Freeform Pen path waiting for its next ⌥-click.
|
||||
crate::lasso_ui::commit(app);
|
||||
}
|
||||
press(app, [x, y], mods, convert)
|
||||
}
|
||||
ToolEvent::Move { x, y, .. } => {
|
||||
|
||||
@@ -2563,7 +2563,7 @@ cameraData Lens Objektiv
|
||||
Freeform Pen Tool Nástroj pero od ruky
|
||||
Curvature Pen Tool Nástroj pero křivosti
|
||||
Curve Fit: Přizpůsobení křivky:
|
||||
Drag: draw freehand · Release on the start point: close Tažení: kreslit od ruky · uvolnění na počátečním bodě: uzavřít
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close Tažení: kreslit od ruky · {alt}+klik: rovný segment · uvolnění na počátečním bodě: uzavřít
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish Kliknutí: hladký bod · dvojklik: roh · tažení bodu: přesunout · kliknutí na první bod: uzavřít · {key} dokončit
|
||||
New Crop Preset… Nová předvolba oříznutí…
|
||||
Delete Crop Preset… Odstranit předvolbu oříznutí…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2594,7 +2594,7 @@ cameraData Lens Objektiv
|
||||
Freeform Pen Tool Freiform-Stift-Werkzeug
|
||||
Curvature Pen Tool Rundungsstift-Werkzeug
|
||||
Curve Fit: Kurvenanpassung:
|
||||
Drag: draw freehand · Release on the start point: close Ziehen: frei zeichnen · Am Startpunkt loslassen: schließen
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close Ziehen: frei zeichnen · {alt}-Klick: gerades Segment · Am Startpunkt loslassen: schließen
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish Klick: weicher Punkt · Doppelklick: Ecke · Punkt ziehen: verschieben · Ersten Punkt klicken: schließen · {key} fertig
|
||||
New Crop Preset… Neue Freistellungsvorgabe…
|
||||
Delete Crop Preset… Freistellungsvorgabe löschen…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3654,7 +3654,7 @@ cameraData Lens Φακός
|
||||
Freeform Pen Tool Πένα ελεύθερης σχεδίασης
|
||||
Curvature Pen Tool Πένα καμπυλότητας
|
||||
Curve Fit: Προσαρμογή καμπύλης:
|
||||
Drag: draw freehand · Release on the start point: close Σύρσιμο: ελεύθερη σχεδίαση · Άφημα στο αρχικό σημείο: κλείσιμο
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close Σύρσιμο: ελεύθερη σχεδίαση · {alt}-κλικ: ευθύ τμήμα · Άφημα στο αρχικό σημείο: κλείσιμο
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish Κλικ: ομαλό σημείο · Διπλό κλικ: γωνία · Σύρσιμο σημείου: μετακίνηση · Κλικ στο πρώτο σημείο: κλείσιμο · {key}: τέλος
|
||||
New Crop Preset… Νέα προεπιλογή περικοπής…
|
||||
Delete Crop Preset… Διαγραφή προεπιλογής περικοπής…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1000 and column 18.
|
@@ -2635,7 +2635,7 @@ cameraData Lens Objetivo
|
||||
Freeform Pen Tool Herramienta Pluma de forma libre
|
||||
Curvature Pen Tool Herramienta Pluma de curvatura
|
||||
Curve Fit: Ajuste de curva:
|
||||
Drag: draw freehand · Release on the start point: close Arrastrar: dibujar a mano alzada · Soltar en el punto inicial: cerrar
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close Arrastrar: dibujar a mano alzada · {alt}-clic: segmento recto · Soltar en el punto inicial: cerrar
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish Clic: punto suave · Doble clic: esquina · Arrastrar un punto: mover · Clic en el primer punto: cerrar · {key}: terminar
|
||||
New Crop Preset… Nuevo ajuste preestablecido de recorte…
|
||||
Delete Crop Preset… Eliminar ajuste preestablecido de recorte…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -2972,7 +2972,7 @@ cameraData Lens Objectif
|
||||
Freeform Pen Tool Outil Plume libre
|
||||
Curvature Pen Tool Outil Plume de courbure
|
||||
Curve Fit: Ajustement de courbe :
|
||||
Drag: draw freehand · Release on the start point: close Glisser : dessin à main levée · Relâcher sur le point de départ : fermer
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close Glisser : dessin à main levée · {alt}-clic : segment droit · Relâcher sur le point de départ : fermer
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish Clic : point lisse · Double-clic : angle · Glisser un point : déplacer · Clic sur le premier point : fermer · {key} : terminer
|
||||
New Crop Preset… Nouveau paramètre prédéfini de recadrage…
|
||||
Delete Crop Preset… Supprimer le paramètre prédéfini de recadrage…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2959,7 +2959,7 @@ cameraData Lens Lensa
|
||||
Freeform Pen Tool Alat Pen Bebas
|
||||
Curvature Pen Tool Alat Pen Kelengkungan
|
||||
Curve Fit: Penyesuaian Kurva:
|
||||
Drag: draw freehand · Release on the start point: close Seret: gambar bebas · Lepas di titik awal: tutup
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close Seret: gambar bebas · {alt}-klik: segmen lurus · Lepas di titik awal: tutup
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish Klik: titik halus · Klik ganda: sudut · Seret titik: pindahkan · Klik titik pertama: tutup · {key} selesai
|
||||
New Crop Preset… Preset Pangkas Baru…
|
||||
Delete Crop Preset… Hapus Preset Pangkas…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -2626,7 +2626,7 @@ cameraData Lens Obiettivo
|
||||
Freeform Pen Tool Strumento Penna a mano libera
|
||||
Curvature Pen Tool Strumento Penna curvatura
|
||||
Curve Fit: Adattamento curva:
|
||||
Drag: draw freehand · Release on the start point: close Trascina: disegna a mano libera · Rilascia sul punto iniziale: chiudi
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close Trascina: disegna a mano libera · {alt}-clic: segmento retto · Rilascia sul punto iniziale: chiudi
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish Click: punto morbido · Doppio click: angolo · Trascina un punto: sposta · Click primo punto: chiudi · {key} termina
|
||||
New Crop Preset… Nuovo predefinito di ritaglio…
|
||||
Delete Crop Preset… Elimina predefinito di ritaglio…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2586 and column 53.
|
@@ -3568,7 +3568,7 @@ cameraData Lens レンズ
|
||||
Freeform Pen Tool フリーフォームペンツール
|
||||
Curvature Pen Tool 曲線ペンツール
|
||||
Curve Fit: カーブフィット:
|
||||
Drag: draw freehand · Release on the start point: close ドラッグ: フリーハンドで描画 · 始点で離す: 閉じる
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close ドラッグ: フリーハンドで描画 · {alt}+クリック: 直線セグメント · 始点で離す: 閉じる
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish クリック: スムーズポイント · ダブルクリック: 角 · 点をドラッグ: 移動 · 最初の点をクリック: 閉じる · {key} 完了
|
||||
New Crop Preset… 新規切り抜きプリセット…
|
||||
Delete Crop Preset… 切り抜きプリセットを削除…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3137,7 +3137,7 @@ cameraData Lens 렌즈
|
||||
Freeform Pen Tool 자유 형태 펜 도구
|
||||
Curvature Pen Tool 곡률 펜 도구
|
||||
Curve Fit: 곡선 맞춤:
|
||||
Drag: draw freehand · Release on the start point: close 드래그: 자유롭게 그리기 · 시작점에서 놓기: 닫기
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close 드래그: 자유롭게 그리기 · {alt}-클릭: 직선 세그먼트 · 시작점에서 놓기: 닫기
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish 클릭: 매끄러운 점 · 더블 클릭: 모서리 · 점 드래그: 이동 · 첫 점 클릭: 닫기 · {key}: 완료
|
||||
New Crop Preset… 새 자르기 사전 설정…
|
||||
Delete Crop Preset… 자르기 사전 설정 삭제…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3095 and column 53.
|
@@ -3714,7 +3714,7 @@ cameraData Lens Lens
|
||||
Freeform Pen Tool Pengereedschap Vrije vorm
|
||||
Curvature Pen Tool Pengereedschap Kromming
|
||||
Curve Fit: Curve aanpassen:
|
||||
Drag: draw freehand · Release on the start point: close Slepen: uit de vrije hand tekenen · Loslaten op beginpunt: sluiten
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close Slepen: uit de vrije hand tekenen · {alt}-klik: recht segment · Loslaten op beginpunt: sluiten
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish Klik: vloeiend punt · Dubbelklik: hoek · Punt slepen: verplaatsen · Klik op eerste punt: sluiten · {key} voltooien
|
||||
New Crop Preset… Nieuwe uitsnijvoorinstelling…
|
||||
Delete Crop Preset… Uitsnijvoorinstelling verwijderen…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
@@ -3570,7 +3570,7 @@ cameraData Lens Obiektyw
|
||||
Freeform Pen Tool Narzędzie Pióro dowolne
|
||||
Curvature Pen Tool Narzędzie Pióro krzywizny
|
||||
Curve Fit: Dopasowanie krzywej:
|
||||
Drag: draw freehand · Release on the start point: close Przeciągnięcie: rysowanie odręczne · Puszczenie w punkcie początkowym: zamknij
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close Przeciągnięcie: rysowanie odręczne · {alt}-klik: prosty segment · Puszczenie w punkcie początkowym: zamknij
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish Klik: punkt gładki · Dwuklik: narożnik · Przeciągnięcie punktu: przesuń · Klik w pierwszy punkt: zamknij · {key} zakończ
|
||||
New Crop Preset… Nowe ustawienie kadrowania…
|
||||
Delete Crop Preset… Usuń ustawienie kadrowania…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2977,7 +2977,7 @@ cameraData Lens Lente
|
||||
Freeform Pen Tool Ferramenta Caneta de forma livre
|
||||
Curvature Pen Tool Ferramenta Caneta de curvatura
|
||||
Curve Fit: Ajuste de curva:
|
||||
Drag: draw freehand · Release on the start point: close Arrastar: desenhar à mão livre · Soltar no ponto inicial: fechar
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close Arrastar: desenhar à mão livre · {alt}-clique: segmento reto · Soltar no ponto inicial: fechar
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish Clique: ponto suave · Clique duplo: canto · Arrastar um ponto: mover · Clique no primeiro ponto: fechar · {key}: concluir
|
||||
New Crop Preset… Nova predefinição de corte…
|
||||
Delete Crop Preset… Excluir predefinição de corte…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2899,7 +2899,7 @@ cameraData Lens Объектив
|
||||
Freeform Pen Tool Свободное перо
|
||||
Curvature Pen Tool Перо кривизны
|
||||
Curve Fit: Точность кривой:
|
||||
Drag: draw freehand · Release on the start point: close Перетаскивание: рисовать от руки · отпустить на начальной точке: замкнуть
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close Перетаскивание: рисовать от руки · {alt}-щелчок: прямой сегмент · отпустить на начальной точке: замкнуть
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish Клик: гладкая точка · двойной клик: угол · перетаскивание точки: переместить · клик по первой точке: замкнуть · {key} завершить
|
||||
Scale: Масштаб:
|
||||
Snap to Origin Привязать к началу координат
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3651,7 +3651,7 @@ cameraData Lens Об'єктив
|
||||
Freeform Pen Tool Інструмент «Довільне перо»
|
||||
Curvature Pen Tool Інструмент «Перо кривизни»
|
||||
Curve Fit: Точність кривої:
|
||||
Drag: draw freehand · Release on the start point: close Перетягування: малювати від руки · Відпускання на початковій точці: замкнути
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close Перетягування: малювати від руки · {alt}+клацання: прямий сегмент · Відпускання на початковій точці: замкнути
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish Клацання: плавна точка · Подвійне клацання: кут · Перетягування точки: перемістити · Клацання першої точки: замкнути · {key}: завершити
|
||||
Search settings… Пошук налаштувань…
|
||||
No matching settings Немає відповідних налаштувань
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1356 and column 18.
|
@@ -2570,7 +2570,7 @@ cameraData Lens 镜头
|
||||
Freeform Pen Tool 自由钢笔工具
|
||||
Curvature Pen Tool 弯度钢笔工具
|
||||
Curve Fit: 曲线拟合:
|
||||
Drag: draw freehand · Release on the start point: close 拖动:自由绘制 · 在起点松开:闭合
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close 拖动:自由绘制 · {alt}-单击:直线段 · 在起点松开:闭合
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish 单击:平滑点 · 双击:角点 · 拖动点:移动 · 单击起点:闭合 · {key} 完成
|
||||
Scale: 缩放:
|
||||
Snap to Origin 贴紧原点
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2528 and column 53.
|
@@ -2540,7 +2540,7 @@ cameraData Lens 鏡頭
|
||||
Freeform Pen Tool 自由筆型工具
|
||||
Curvature Pen Tool 曲線筆工具
|
||||
Curve Fit: 曲線擬合:
|
||||
Drag: draw freehand · Release on the start point: close 拖曳:手繪 · 在起點放開:閉合
|
||||
Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close 拖曳:手繪 · {alt}-按一下:直線段 · 在起點放開:閉合
|
||||
Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish 按一下:平滑點 · 按兩下:轉角 · 拖曳點:移動 · 按一下第一點:閉合 · {key} 完成
|
||||
Scale: 縮放:
|
||||
Snap to Origin 靠齊原點
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4,6 +4,10 @@
|
||||
//! Selection intent is latched at the first press: Alt held then subtracts from an existing
|
||||
//! selection; with no selection there is nothing to subtract, so it starts straight segments.
|
||||
//! Alt pressed later changes drawing mode.
|
||||
//!
|
||||
//! The Freeform Pen draws the same way (Photoshop): a drag is freehand, Alt-clicks add straight
|
||||
//! segments, releasing Alt with the button down goes on freehand and with it up finishes the
|
||||
//! path (`vector_ui::freeform_finish`, which keeps the straight segments straight).
|
||||
|
||||
use crate::{
|
||||
PhotocraftApp,
|
||||
@@ -18,20 +22,34 @@ pub struct Lasso {
|
||||
down: bool,
|
||||
polygonal: bool,
|
||||
document: Option<photocraft_doc::DocId>,
|
||||
/// Indices of the points an Alt-click added: the segment into each one is straight.
|
||||
straight: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Lasso {
|
||||
/// Indices into the drag's points whose incoming segment is straight (an Alt-click).
|
||||
pub fn straight(&self) -> &[usize] {
|
||||
&self.straight
|
||||
}
|
||||
}
|
||||
|
||||
/// The tools this module draws with: the Lasso and the Freeform Pen.
|
||||
pub fn owns(tool: Tool) -> bool {
|
||||
matches!(tool, Tool::Lasso | Tool::FreeformPen)
|
||||
}
|
||||
|
||||
pub fn active(app: &PhotocraftApp) -> bool {
|
||||
app.drag.as_ref().is_some_and(|d| d.tool == Tool::Lasso && d.lasso.is_some())
|
||||
app.drag.as_ref().is_some_and(|d| owns(d.tool) && d.lasso.is_some())
|
||||
}
|
||||
|
||||
/// Between polygonal clicks, Backspace/right-click retracts the last fixed vertex.
|
||||
/// Do not affect a freehand stroke whose primary button is still held.
|
||||
pub fn waiting_for_vertex(app: &PhotocraftApp) -> bool {
|
||||
app.ui.tool == Tool::Lasso
|
||||
owns(app.ui.tool)
|
||||
&& app
|
||||
.drag
|
||||
.as_ref()
|
||||
.is_some_and(|d| d.tool == Tool::Lasso && d.lasso.as_ref().is_some_and(|l| !l.down && l.document == app.session.active().map(|st| st.doc.id)))
|
||||
.is_some_and(|d| d.tool == app.ui.tool && d.lasso.as_ref().is_some_and(|l| !l.down && l.document == app.session.active().map(|st| st.doc.id)))
|
||||
}
|
||||
|
||||
pub fn undo_last_vertex(app: &mut PhotocraftApp) -> bool {
|
||||
@@ -43,6 +61,10 @@ pub fn undo_last_vertex(app: &mut PhotocraftApp) -> bool {
|
||||
app.drag = None;
|
||||
} else if let Some(d) = app.drag.as_mut() {
|
||||
d.points.pop();
|
||||
let n = d.points.len();
|
||||
if let Some(lasso) = &mut d.lasso {
|
||||
lasso.straight.retain(|&i| i < n);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -54,19 +76,20 @@ fn moving_selection(app: &PhotocraftApp) -> bool {
|
||||
}
|
||||
|
||||
/// A drag the canvas handles rather than this module, which only feeds it the events: moving the
|
||||
/// selection, a guide that ⌘ took (`canvas::command_guide_at`, #2690), or the layer that ⌘ moves
|
||||
/// (`canvas::command_moves_layer`, refused on a locked layer: `move_blocked`, #2725).
|
||||
/// selection, a guide that ⌘ took (`canvas::command_guide_at`, #2690), the layer that ⌘ moves
|
||||
/// (`canvas::command_moves_layer`, refused on a locked layer: `move_blocked`, #2725), or a path
|
||||
/// that the Freeform Pen's ⌘ (Direct Selection) edits.
|
||||
fn handed_over(app: &PhotocraftApp) -> bool {
|
||||
moving_selection(app) || app.guide_drag.is_some_and(|d| d.index.is_some()) || app.drag.as_ref().is_some_and(|d| d.tool == Tool::Move) || app.move_blocked
|
||||
moving_selection(app)
|
||||
|| app.guide_drag.is_some_and(|d| d.index.is_some())
|
||||
|| app.drag.as_ref().is_some_and(|d| d.tool == Tool::Move)
|
||||
|| app.move_blocked
|
||||
|| app.ui.direct_selection.drag.is_some()
|
||||
}
|
||||
|
||||
pub fn cancel_stale(app: &mut PhotocraftApp) {
|
||||
if app
|
||||
.drag
|
||||
.as_ref()
|
||||
.and_then(|d| d.lasso.as_ref())
|
||||
.is_some_and(|lasso| app.ui.tool != Tool::Lasso || lasso.document != app.session.active().map(|st| st.doc.id))
|
||||
{
|
||||
let document = app.session.active().map(|st| st.doc.id);
|
||||
if app.drag.as_ref().is_some_and(|d| d.lasso.as_ref().is_some_and(|lasso| app.ui.tool != d.tool || lasso.document != document)) {
|
||||
app.drag = None;
|
||||
}
|
||||
}
|
||||
@@ -78,7 +101,7 @@ fn append(d: &mut Drag, p: [f64; 2]) {
|
||||
}
|
||||
|
||||
fn modifiers(app: &mut PhotocraftApp, mods: Modifiers) {
|
||||
let Some(d) = app.drag.as_mut().filter(|d| d.tool == Tool::Lasso) else { return };
|
||||
let Some(d) = app.drag.as_mut().filter(|d| owns(d.tool)) else { return };
|
||||
d.track(mods);
|
||||
// Alt held before the initial press still means subtract, until released and pressed again.
|
||||
let polygonal = mods.alt && (!d.modifiers.alt || d.released.alt);
|
||||
@@ -107,7 +130,8 @@ pub fn commit(app: &mut PhotocraftApp) {
|
||||
|
||||
pub fn pointer(app: &mut PhotocraftApp, ev: ToolEvent, mods: Modifiers) -> bool {
|
||||
cancel_stale(app);
|
||||
if app.ui.tool != Tool::Lasso || handed_over(app) {
|
||||
let tool = app.ui.tool;
|
||||
if !owns(tool) || handed_over(app) {
|
||||
return false;
|
||||
}
|
||||
let p = match ev {
|
||||
@@ -116,6 +140,7 @@ pub fn pointer(app: &mut PhotocraftApp, ev: ToolEvent, mods: Modifiers) -> bool
|
||||
// A press inside the selection (no outline in progress) drags the selection instead; a ⌘
|
||||
// press outside it moves the layer, as the Move tool (#2725).
|
||||
if matches!(ev, ToolEvent::Down { .. })
|
||||
&& tool == Tool::Lasso
|
||||
&& !active(app)
|
||||
&& p.iter().all(|n| n.is_finite())
|
||||
&& (crate::canvas::selection_drag_kind(app, Tool::Lasso, p, mods).is_some() || crate::canvas::command_moves_layer(app, Tool::Lasso, p, mods))
|
||||
@@ -134,30 +159,45 @@ pub fn pointer(app: &mut PhotocraftApp, ev: ToolEvent, mods: Modifiers) -> bool
|
||||
let near = |q: [f64; 2]| (q[0] - p[0]).hypot(q[1] - p[1]) < reach;
|
||||
let close = app.drag.as_ref().is_some_and(|d| d.points.len() >= 3 && (near(d.start) || d.points.last().is_some_and(|l| near([l[0], l[1]]))));
|
||||
if close {
|
||||
// A path closes on its start point (a selection outline always closes).
|
||||
if let Some(d) = app.drag.as_mut().filter(|d| d.tool == Tool::FreeformPen && near(d.start)) {
|
||||
let (start, n) = (d.start, d.points.len());
|
||||
append(d, start);
|
||||
if let Some(lasso) = d.lasso.as_mut().filter(|_| d.points.len() > n) {
|
||||
lasso.straight.push(n);
|
||||
}
|
||||
}
|
||||
commit(app);
|
||||
return true;
|
||||
}
|
||||
if let Some(d) = &mut app.drag {
|
||||
let n = d.points.len();
|
||||
append(d, p);
|
||||
let added = d.points.len() > n;
|
||||
if let Some(lasso) = &mut d.lasso {
|
||||
lasso.cursor = p;
|
||||
lasso.down = true;
|
||||
if added {
|
||||
lasso.straight.push(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Alt with no selection has nothing to subtract from: the outline starts with
|
||||
// straight segments, as a new selection.
|
||||
let nothing_selected = app.session.active().is_none_or(|st| st.doc.selection.is_none());
|
||||
// A path has nothing to subtract from either.
|
||||
let nothing_selected = tool == Tool::FreeformPen || app.session.active().is_none_or(|st| st.doc.selection.is_none());
|
||||
let polygonal = mods.alt && nothing_selected;
|
||||
let intent = if polygonal { Modifiers { alt: false, ..mods } } else { mods };
|
||||
let mut d = Drag::new(Tool::Lasso, p, vec![[p[0], p[1], 1.0]], intent, false);
|
||||
d.lasso = Some(Lasso { cursor: p, down: true, polygonal, document: app.session.active().map(|st| st.doc.id) });
|
||||
let mut d = Drag::new(tool, p, vec![[p[0], p[1], 1.0]], intent, false);
|
||||
let document = app.session.active().map(|st| st.doc.id);
|
||||
d.lasso = Some(Lasso { cursor: p, down: true, polygonal, document, straight: Vec::new() });
|
||||
app.drag = Some(d);
|
||||
}
|
||||
}
|
||||
ToolEvent::Move { .. } | ToolEvent::Up { .. } => {
|
||||
let up = matches!(ev, ToolEvent::Up { .. });
|
||||
let Some(d) = app.drag.as_mut().filter(|d| d.tool == Tool::Lasso) else { return true };
|
||||
let Some(d) = app.drag.as_mut().filter(|d| d.tool == tool) else { return true };
|
||||
let Some(lasso) = &d.lasso else { return true };
|
||||
let (down, polygonal, previous) = (lasso.down, lasso.polygonal, lasso.cursor);
|
||||
if d.reposition {
|
||||
|
||||
@@ -104,3 +104,68 @@ fn curvature_pen_clicks_make_a_smooth_path_through_every_point() {
|
||||
assert!(!sp.knots[1].smooth && sp.knots[1].in_ctrl == sp.knots[1].anchor, "a corner point");
|
||||
assert!(sp.knots[0].smooth && sp.knots[2].smooth);
|
||||
}
|
||||
|
||||
fn ev(app: &mut PhotocraftApp, kind: &str, x: f64, y: f64, mods: Modifiers) {
|
||||
let e = match kind {
|
||||
"down" => ToolEvent::Down { x, y, pressure: 1.0 },
|
||||
"up" => ToolEvent::Up { x, y },
|
||||
_ => ToolEvent::Move { x, y, pressure: 1.0 },
|
||||
};
|
||||
tool_event(app, e, mods);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freeform_pen_alt_clicks_draw_straight_segments_between_freehand_runs() {
|
||||
// Photoshop: ⌥ held at the release keeps the path open, each ⌥-click adds a straight segment,
|
||||
// releasing ⌥ with the button down goes on freehand.
|
||||
let mut app = app(Tool::FreeformPen);
|
||||
app.sync_views();
|
||||
let before = app.session.active().unwrap().history.entries().len();
|
||||
ev(&mut app, "down", 20.0, 20.0, Modifiers::NONE);
|
||||
for x in (22..=80).step_by(2) {
|
||||
ev(&mut app, "move", f64::from(x), 20.0, Modifiers::NONE);
|
||||
}
|
||||
ev(&mut app, "move", 80.0, 20.0, Modifiers::ALT);
|
||||
ev(&mut app, "up", 80.0, 20.0, Modifiers::ALT);
|
||||
assert!(app.session.active().unwrap().doc.work_path.is_none(), "⌥ at the release keeps the path open");
|
||||
// Hovering only moves the rubber band; the click adds a straight segment.
|
||||
ev(&mut app, "move", 120.0, 40.0, Modifiers::ALT);
|
||||
ev(&mut app, "down", 80.0, 100.0, Modifiers::ALT);
|
||||
// ⌥ released with the button down: freehand again, a half circle down to (20, 100).
|
||||
for i in 0..=36 {
|
||||
let a = (f64::from(i) * 5.0).to_radians();
|
||||
ev(&mut app, "move", 50.0 + 30.0 * a.cos(), 100.0 + 30.0 * a.sin(), Modifiers::NONE);
|
||||
}
|
||||
ev(&mut app, "up", 20.0, 100.0, Modifiers::NONE);
|
||||
let path = work_path(&app);
|
||||
let knots = &path.subpaths[0].knots;
|
||||
let anchors: Vec<[f64; 2]> = knots.iter().map(|k| [k.anchor.x.round(), k.anchor.y.round()]).collect();
|
||||
assert_eq!(anchors[..3], [[20.0, 20.0], [80.0, 20.0], [80.0, 100.0]], "{anchors:?}");
|
||||
assert_eq!(anchors.last(), Some(&[20.0, 100.0]));
|
||||
assert!(!anchors.contains(&[120.0, 40.0]), "the hover is not a point");
|
||||
// The ⌥-click segment is straight: no handles on either end of it.
|
||||
assert_eq!((knots[1].out_ctrl, knots[2].in_ctrl), (knots[1].anchor, knots[2].anchor));
|
||||
assert!(!knots[1].smooth && !knots[2].smooth);
|
||||
// The freehand run after it curves.
|
||||
assert!(knots[2..].iter().any(|k| k.out_ctrl != k.anchor), "{knots:?}");
|
||||
assert_eq!(app.session.active().unwrap().history.entries().len(), before + 1, "one History step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_pens_with_command_edit_paths_as_direct_selection() {
|
||||
for tool in [Tool::FreeformPen, Tool::CurvaturePen] {
|
||||
let mut app = app(Tool::Pen);
|
||||
click(&mut app, 30.0, 30.0);
|
||||
click(&mut app, 90.0, 30.0);
|
||||
crate::vector_ui::pen_commit(&mut app, false);
|
||||
app.ui.tool = tool;
|
||||
// ⌘-drag (Ctrl off the Mac) on an anchor moves it; the tool stays.
|
||||
ev(&mut app, "down", 90.0, 30.0, Modifiers::COMMAND);
|
||||
ev(&mut app, "move", 90.0, 70.0, Modifiers::COMMAND);
|
||||
ev(&mut app, "up", 90.0, 70.0, Modifiers::COMMAND);
|
||||
let wp = work_path(&app);
|
||||
assert_eq!(wp.subpaths.len(), 1, "{tool:?} drew nothing");
|
||||
assert_eq!([wp.subpaths[0].knots[1].anchor.x, wp.subpaths[0].knots[1].anchor.y], [90.0, 70.0], "{tool:?}");
|
||||
assert_eq!(app.ui.tool, tool);
|
||||
}
|
||||
}
|
||||
@@ -379,17 +379,80 @@ fn commit_path(app: &mut PhotocraftApp, pen: &PenPath, closed: bool) {
|
||||
// Freeform Pen and Curvature Pen
|
||||
|
||||
/// Freeform Pen release: fit the drag's trace within the options bar's Curve Fit and commit it
|
||||
/// as the Pen does. Releasing on the start point closes the path; a click draws nothing.
|
||||
pub fn freeform_finish(app: &mut PhotocraftApp, points: &[[f64; 3]]) {
|
||||
/// as the Pen does. Releasing on the start point closes the path; a click draws nothing. The
|
||||
/// points at indices `straight` were ⌥-clicked: the segment into each one stays straight.
|
||||
pub fn freeform_finish(app: &mut PhotocraftApp, points: &[[f64; 3]], straight: &[usize]) {
|
||||
let trace: Vec<[f64; 2]> = points.iter().map(|p| [p[0], p[1]]).collect();
|
||||
let (Some(first), Some(last)) = (trace.first(), trace.last()) else { return };
|
||||
let tol = 6.0 / app.point_zoom().max(0.01) as f64;
|
||||
let length: f64 = trace.windows(2).map(|w| (w[1][0] - w[0][0]).hypot(w[1][1] - w[0][1])).sum();
|
||||
let closed = (first[0] - last[0]).hypot(first[1] - last[1]) < tol && length > 4.0 * tol;
|
||||
let Some(sub) = photocraft_vector::trace::fit_trace(&trace, f64::from(app.ui.tool_options.curve_fit), closed) else { return };
|
||||
let knots = sub.knots.iter().map(|k| [[k.anchor.x, k.anchor.y], [k.in_ctrl.x, k.in_ctrl.y], [k.out_ctrl.x, k.out_ctrl.y]]).collect();
|
||||
let unlinked = sub.knots.iter().enumerate().filter(|(_, k)| !k.smooth).map(|(i, _)| i).collect();
|
||||
commit_path(app, &PenPath { knots, unlinked, ..Default::default() }, sub.closed);
|
||||
let fit = f64::from(app.ui.tool_options.curve_fit);
|
||||
let pen = if straight.is_empty() {
|
||||
let Some(sub) = photocraft_vector::trace::fit_trace(&trace, fit, closed) else { return };
|
||||
let knots = sub.knots.iter().map(knot_points).collect();
|
||||
let unlinked = sub.knots.iter().enumerate().filter(|(_, k)| !k.smooth).map(|(i, _)| i).collect();
|
||||
(PenPath { knots, unlinked, ..Default::default() }, sub.closed)
|
||||
} else {
|
||||
fit_with_straight_segments(&trace, straight, fit, closed.then_some(tol))
|
||||
};
|
||||
commit_path(app, &pen.0, pen.1);
|
||||
}
|
||||
|
||||
fn knot_points(k: &photocraft_doc::Knot) -> [[f64; 2]; 3] {
|
||||
[[k.anchor.x, k.anchor.y], [k.in_ctrl.x, k.in_ctrl.y], [k.out_ctrl.x, k.out_ctrl.y]]
|
||||
}
|
||||
|
||||
/// A Freeform Pen trace with ⌥-clicked straight segments (into the points at `straight`): each
|
||||
/// freehand run between them is fitted on its own, and the runs are joined by straight lines
|
||||
/// between corner anchors. `close` (a reach) closes the path when it ends on its start point.
|
||||
fn fit_with_straight_segments(trace: &[[f64; 2]], straight: &[usize], fit: f64, close: Option<f64>) -> (PenPath, bool) {
|
||||
let mut cuts: Vec<usize> = straight.iter().copied().filter(|&i| i > 0 && i < trace.len()).collect();
|
||||
cuts.sort_unstable();
|
||||
cuts.dedup();
|
||||
cuts.push(trace.len());
|
||||
let (mut knots, mut corners): (Vec<[[f64; 2]; 3]>, Vec<usize>) = (Vec::new(), Vec::new());
|
||||
let mut start = 0;
|
||||
for end in cuts {
|
||||
let run = trace.get(start..end).unwrap_or_default();
|
||||
let first = knots.len();
|
||||
match photocraft_vector::trace::fit_trace(run, fit, false) {
|
||||
Some(sub) => {
|
||||
corners.extend(sub.knots.iter().enumerate().filter(|(_, k)| !k.smooth).map(|(i, _)| first + i));
|
||||
knots.extend(sub.knots.iter().map(knot_points));
|
||||
}
|
||||
// A single click (or a run that never moved): one corner anchor.
|
||||
None => knots.extend(run.first().map(|p| [*p; 3])),
|
||||
}
|
||||
// Where a straight segment meets the run: a corner with no handle on that side.
|
||||
if let Some(k) = knots.get_mut(first).filter(|_| first > 0) {
|
||||
k[1] = k[0];
|
||||
corners.push(first);
|
||||
}
|
||||
if end < trace.len()
|
||||
&& let Some(k) = knots.last_mut()
|
||||
{
|
||||
k[2] = k[0];
|
||||
corners.push(knots.len() - 1);
|
||||
}
|
||||
start = end;
|
||||
}
|
||||
// Ended on the start point: the last anchor is the first one.
|
||||
let ends_on_start = |k: &[[[f64; 2]; 3]], reach: f64| match (k.first(), k.last()) {
|
||||
(Some(a), Some(b)) => k.len() > 3 && (a[0][0] - b[0][0]).hypot(a[0][1] - b[0][1]) < reach,
|
||||
_ => false,
|
||||
};
|
||||
let closed = close.is_some_and(|reach| ends_on_start(&knots, reach));
|
||||
if closed && let Some(last) = knots.pop() {
|
||||
if let Some(k) = knots.first_mut() {
|
||||
k[1] = [last[1][0] - last[0][0] + k[0][0], last[1][1] - last[0][1] + k[0][1]];
|
||||
}
|
||||
corners.retain(|&i| i < knots.len());
|
||||
corners.push(0);
|
||||
}
|
||||
corners.sort_unstable();
|
||||
corners.dedup();
|
||||
(PenPath { knots, unlinked: corners, ..Default::default() }, closed)
|
||||
}
|
||||
|
||||
/// Recompute a Curvature Pen path's handles from its anchors and corner points.
|
||||
@@ -806,7 +869,8 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui, tool: Tool) -> bo
|
||||
crate::widgets::vline(ui, 22.0);
|
||||
lbl(ui, tl!("Curve Fit:"));
|
||||
crate::widgets::value_field(ui, &mut o.curve_fit, 0.5..=10.0, "px", 58.0);
|
||||
lbl(ui, tl!("Drag: draw freehand · Release on the start point: close"));
|
||||
let alt = crate::shortcuts::pretty("Alt");
|
||||
lbl(ui, &crate::i18n::fmt(tl!("Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close"), &[("alt", &alt)]));
|
||||
}
|
||||
Tool::CurvaturePen => lbl(
|
||||
ui,
|
||||
|
||||
Reference in new issue
Block a user