mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-16 20:02:20 +02:00
feat(photo-editor): Ajax Image Editor client (Photoshop-parity tools)
Photoshop-parity editor client: layers, selection/masking, brushes,
smart filters, LUT import, Quick Edit, mobile gestures, workspaces,
batch operations, and the service-worker versioning (v978->v1006).
Source: vendor/photocraft (ad1cff44) photo-editor client files, merged
onto upstream/dev (a8c147b2). Conflict resolution documented in
pr_work/A_conflicts.md; editor files taken from vendor/photocraft
(upstream's editor is an earlier snapshot of the same feature).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c74f475679
commit
5fac693c1c
@@ -0,0 +1,118 @@
|
||||
import json
|
||||
import subprocess
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MODULE = (ROOT / "static/js/editor/canvas-context-menu.js").as_uri()
|
||||
STATE = (ROOT / "static/js/editor/state.js").as_uri()
|
||||
|
||||
|
||||
def run_node(body: str):
|
||||
result = subprocess.run(
|
||||
["node", "--input-type=module", "-e", textwrap.dedent(body)],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def labels(items):
|
||||
out = []
|
||||
for item in items:
|
||||
if item.get("type") == "sep":
|
||||
out.append("—")
|
||||
else:
|
||||
out.append(item["label"])
|
||||
return out
|
||||
|
||||
|
||||
def menu_for(tool: str, selection: bool = False):
|
||||
return run_node(
|
||||
f"""
|
||||
import {{ state }} from {json.dumps(STATE)};
|
||||
import {{ buildCanvasMenuItems }} from {json.dumps(MODULE)};
|
||||
state.editorOpen = true;
|
||||
state.tool = {json.dumps(tool)};
|
||||
state.wandMask = {json.dumps({"w": 1, "h": 1} if selection else None)};
|
||||
state.brushPixel = false;
|
||||
const items = buildCanvasMenuItems(state.tool);
|
||||
const out = items.map((i) => ({{ type: i.type || 'item', label: i.label || '', disabled: !!i.disabled, hint: i.hint || '' }}));
|
||||
console.log(JSON.stringify(out));
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_paint_tool_menu_has_brush_controls():
|
||||
items = menu_for("brush")
|
||||
ls = labels(items)
|
||||
assert "Increase Brush Size" in ls
|
||||
assert "Decrease Brush Size" in ls
|
||||
assert "Brush Preset" in ls
|
||||
assert "Pixel Brush" in ls
|
||||
# Common edit actions are always available.
|
||||
assert "Undo" in ls and "Redo" in ls
|
||||
assert "Rotate 90° CW" in ls
|
||||
|
||||
|
||||
def test_pixel_brush_entry_is_checked_when_pixel_mode_active():
|
||||
items = run_node(
|
||||
f"""
|
||||
import {{ state }} from {json.dumps(STATE)};
|
||||
import {{ buildCanvasMenuItems }} from {json.dumps(MODULE)};
|
||||
state.tool = 'brush';
|
||||
state.brushPixel = true;
|
||||
const items = buildCanvasMenuItems('brush');
|
||||
const pixel = items.find((i) => i.label === 'Pixel Brush');
|
||||
console.log(JSON.stringify({{ checked: !!pixel.checked }}));
|
||||
"""
|
||||
)
|
||||
assert items["checked"] is True
|
||||
|
||||
|
||||
def test_selection_tool_menu_has_selection_ops():
|
||||
ls = labels(menu_for("marquee", selection=True))
|
||||
assert "Select All" in ls
|
||||
assert "Deselect" in ls
|
||||
assert "Inverse Selection" in ls
|
||||
assert "Feather…" in ls
|
||||
assert "Expand" in ls
|
||||
assert "Contract" in ls
|
||||
|
||||
|
||||
def test_selection_items_disabled_without_selection():
|
||||
items = menu_for("brush", selection=False)
|
||||
by_label = {i["label"]: i for i in items if i["type"] == "item"}
|
||||
assert by_label["Cut"]["disabled"] is True
|
||||
assert by_label["Copy"]["disabled"] is True
|
||||
assert by_label["Paste"]["disabled"] is False
|
||||
|
||||
|
||||
def test_crop_and_transform_tools_have_context_actions():
|
||||
crop = labels(menu_for("crop"))
|
||||
assert "Apply Crop" in crop
|
||||
assert "Cancel Crop" in crop
|
||||
transform = labels(menu_for("transform"))
|
||||
assert "Commit Transform" in transform
|
||||
assert "Cancel Transform" in transform
|
||||
|
||||
|
||||
def test_context_menu_is_wired_into_canvas_events():
|
||||
events = (ROOT / "static/js/editor/canvas-events.js").read_text()
|
||||
assert "import { wireCanvasContextMenu } from './canvas-context-menu.js';" in events
|
||||
assert "wireCanvasContextMenu(canvasArea, { signal });" in events
|
||||
module = (ROOT / "static/js/editor/canvas-context-menu.js").read_text()
|
||||
assert "export function buildCanvasMenuItems(" in module
|
||||
assert "export function openCanvasContextMenu(" in module
|
||||
assert "window.geEditorControl?.invoke(" in module
|
||||
assert "canvas.addEventListener('contextmenu'" in module
|
||||
|
||||
|
||||
def test_context_menu_styles_exist():
|
||||
css = (ROOT / "static/css/13-image-editor.css").read_text()
|
||||
assert ".ge-canvas-menu {" in css
|
||||
assert ".ge-canvas-menu-item" in css
|
||||
assert ".ge-canvas-menu-sub" in css
|
||||
Reference in New Issue
Block a user