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,90 @@
|
||||
import json
|
||||
import subprocess
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MODULE = (ROOT / "static/js/editor/layer-hit-test.js").as_uri()
|
||||
|
||||
|
||||
def run_node(body: str):
|
||||
script = textwrap.dedent(
|
||||
f"""
|
||||
import {{ pickTopmostLayerAt, AUTO_SELECT_ALPHA_THRESHOLD }} from {json.dumps(MODULE)};
|
||||
{body}
|
||||
"""
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "--input-type=module", "-e", script],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_pick_topmost_opaque_layer_skips_transparent_and_special_layers():
|
||||
result = run_node(
|
||||
"""
|
||||
const mk = (id, extra = {}) => ({id, visible: true, canvas: {width: 4, height: 4}, ...extra});
|
||||
// Special layer kinds sit ABOVE an opaque raster so the walk must skip them.
|
||||
const layers = [
|
||||
mk('opaque'),
|
||||
mk('transparent'),
|
||||
mk('hidden', {visible: false}),
|
||||
mk('adjustment', {kind: 'adjustment'}),
|
||||
];
|
||||
const alpha = {opaque: 255, transparent: 0};
|
||||
const over = pickTopmostLayerAt({
|
||||
layers,
|
||||
point: {x: 1, y: 1},
|
||||
sampleAlpha: (layer, x, y) => alpha[layer.id] ?? null,
|
||||
});
|
||||
// A genuinely topmost opaque layer still wins over a lower opaque one.
|
||||
const stacked = pickTopmostLayerAt({
|
||||
layers: [mk('under'), mk('over')],
|
||||
point: {x: 1, y: 1},
|
||||
sampleAlpha: () => 255,
|
||||
});
|
||||
console.log(JSON.stringify({over: over && over.id, stacked: stacked && stacked.id, threshold: AUTO_SELECT_ALPHA_THRESHOLD}));
|
||||
"""
|
||||
)
|
||||
assert result == {"over": "opaque", "stacked": "over", "threshold": 8}
|
||||
|
||||
|
||||
def test_pick_respects_offsets_bounds_locks_and_hidden_groups():
|
||||
result = run_node(
|
||||
"""
|
||||
const mk = (id) => ({id, visible: true, canvas: {width: 10, height: 10}});
|
||||
const layers = [mk('offsetHit'), mk('far'), mk('locked'), mk('inHiddenGroup')];
|
||||
const offsets = {offsetHit: {x: 5, y: 5}, far: {x: 0, y: 0}, locked: {x: 0, y: 0}, inHiddenGroup: {x: 0, y: 0}};
|
||||
const picked = pickTopmostLayerAt({
|
||||
layers,
|
||||
point: {x: 6, y: 6},
|
||||
isLocked: layer => layer.id === 'locked',
|
||||
isHidden: layer => layer.id === 'inHiddenGroup',
|
||||
getOffset: layer => offsets[layer.id],
|
||||
// 'far' has no rendered pixel under this point.
|
||||
sampleAlpha: (layer, x, y) => (layer.id === 'far' ? null : 255),
|
||||
});
|
||||
console.log(JSON.stringify({picked: picked && picked.id}));
|
||||
"""
|
||||
)
|
||||
assert result == {"picked": "offsetHit"}
|
||||
|
||||
|
||||
def test_pick_returns_null_when_nothing_is_opaque_under_pointer():
|
||||
result = run_node(
|
||||
"""
|
||||
const layers = [{id: 'only', visible: true, canvas: {width: 4, height: 4}}];
|
||||
const picked = pickTopmostLayerAt({
|
||||
layers,
|
||||
point: {x: 100, y: 100},
|
||||
sampleAlpha: () => 255,
|
||||
});
|
||||
console.log(JSON.stringify({picked: picked && picked.id}));
|
||||
"""
|
||||
)
|
||||
assert result == {"picked": None}
|
||||
Reference in New Issue
Block a user