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:
pewdiepie-archdaemon
2026-10-15 11:57:35 +00:00
co-authored by Claude Opus 5.5
parent c74f475679
commit 5fac693c1c
290 changed files with 34358 additions and 766 deletions
+96
View File
@@ -0,0 +1,96 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def _read(*parts):
return (ROOT.joinpath(*parts)).read_text(encoding="utf-8")
def _slice(source, start, end):
return source[source.index(start):source.index(end)]
def test_geometry_has_a_canvas_expanding_renderer():
geometry = _read("static/js/editor", "document-geometry.js")
render = _slice(geometry, "function renderRotatedArbitrary", "function renderFlipped")
# Bounding-box expansion for an arbitrary angle.
assert "const width = Math.abs(source.width * cos) + Math.abs(source.height * sin);" in render
assert "const height = Math.abs(source.width * sin) + Math.abs(source.height * cos);" in render
assert "const out = makeCanvas(Math.ceil(width), Math.ceil(height));" in render
# Smooth resampling and a degenerate-source guard.
assert "ctx.imageSmoothingEnabled = true;" in render
assert "ctx.imageSmoothingQuality = 'high';" in render
assert "if (source && source.width > 0 && source.height > 0)" in render
assert "ctx.translate(out.width / 2, out.height / 2);" in render
assert "ctx.rotate(radians);" in render
assert "ctx.drawImage(source, -source.width / 2, -source.height / 2);" in render
def test_geometry_exports_arbitrary_rotation_with_right_angle_delegation():
geometry = _read("static/js/editor", "document-geometry.js")
rotate = _slice(geometry, "export function rotateDocumentArbitrary", "/** Flip every layer")
assert "export function rotateDocumentArbitrary(editorState, degrees) {" in rotate
assert "const normalized = ((raw % 360) + 360) % 360;" in rotate
# No-op and exact right angles fall back to the existing integer path.
assert "if (normalized === 0) {" in rotate
assert "if ([90, 180, 270].includes(normalized)) return rotateDocument(editorState, normalized);" in rotate
# Centre-anchored mapping into the expanded canvas.
assert "const mapPoint = (x, y) => ({" in rotate
assert "const newWidth = Math.max(1, Math.ceil(Math.abs(oldWidth * cos) + Math.abs(oldHeight * sin)));" in rotate
# Non-destructive layers keep their own transform; rasters are resampled.
assert "rotateTextLayer(layer, normalized);" in rotate
assert "rotateShapeLayer(layer, normalized);" in rotate
assert "layer.placed = transformPlacedData(layer.placed, matrix);" in rotate
assert "replaceCanvas(layer, renderRotatedArbitrary(layer.canvas, normalized));" in rotate
assert "editorState.layerOffsets.set(layer.id, placedOffset || {" in rotate
assert "invalidateLayerCaches(layer);" in rotate
# Masks, selections and seeds travel with the pixels.
assert "mask.space || (mask.mode === 'layer' ? 'layer' : 'document')" in rotate
assert "replaceStandaloneCanvas(editorState.wandMask, renderRotatedArbitrary(editorState.wandMask, normalized));" in rotate
assert "editorState.lassoPoints = (editorState.lassoPoints || []).map(point => mapPoint(point.x, point.y));" in rotate
# Guides can't stay axis-aligned after an arbitrary rotation.
assert "editorState.guides = { vertical: [], horizontal: [] };" in rotate
assert "syncDocumentCanvas(editorState, newWidth, newHeight);" in rotate
assert "return { width: newWidth, height: newHeight, degrees: normalized };" in rotate
def test_canvas_transforms_exposes_rotate_all_arbitrary():
transforms = _read("static/js/editor", "canvas-transforms.js")
assert "rotateDocumentArbitrary" in transforms
method = _slice(transforms, "rotateAllArbitrary(deg)", "flipAll(axis)")
assert "if (!state.layers.length) return;" in method
assert "showCanvasLoading('Rotating…');" in method
assert "const result = rotateDocumentArbitrary(state, deg);" in method
assert "sizeLabel.textContent = `${result.width}×${result.height}`;" in method
assert "fitZoom();" in method
assert "composite();" in method
assert "hideCanvasLoading();" in method
def test_gallery_editor_wires_the_arbitrary_dep_and_control_op():
editor = _read("static/js", "galleryEditor.js")
assert "function _rotateAllLayersArbitrary(deg) { return _canvasTransforms.rotateAllArbitrary(deg); }" in editor
assert "rotateAllLayersArbitrary: (deg) => _rotateAllLayersArbitrary(deg)," in editor
op = _slice(editor, "'document.rotateArbitrary':", "'document.flip':")
assert "Rotate the whole image by an arbitrary angle {degrees}" in op
assert "if (!Number.isFinite(deg)) throw new Error('degrees required');" in op
assert "_rotateAllLayersArbitrary(deg);" in op
assert "return { degrees: deg };" in op
def test_topbar_menu_and_wiring_expose_arbitrary_rotation():
topbar = _read("static/js/editor", "build", "topbar.js")
assert 'data-image-action="rotate-arbitrary"' in topbar
assert "<span>Arbitrary…</span>" in topbar
wiring = _read("static/js/editor", "wire-topbar-menus.js")
assert "rotateAllLayers," in wiring
assert "rotateAllLayersArbitrary," in wiring
assert "flipAllLayers," in wiring
assert ("else if (action === 'rotate-arbitrary') promptNumber("
"'Rotation angle (degrees, clockwise; negative for counter-clockwise)', 15, "
"(v) => rotateAllLayersArbitrary?.(v));") in wiring