mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-27 00:02:21 +01:00
341 lines
12 KiB
Python
341 lines
12 KiB
Python
import json
|
|
import subprocess
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
MODULE = (ROOT / "static/js/editor/transform-frame-geometry.js").as_uri()
|
|
SNAP_MODULE = (ROOT / "static/js/editor/snap.js").as_uri()
|
|
|
|
|
|
def run_node(body: str):
|
|
script = textwrap.dedent(
|
|
f"""
|
|
import {{transformFrameGeometry, hitTestTransformHandle, pointInTransformFrame, resizeTransformFrame, skewTransformFrame, distortTransformFrame}} 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_frame_geometry_exposes_all_resize_handles_from_one_rotated_frame():
|
|
result = run_node(
|
|
"""
|
|
const geometry = transformFrameGeometry({
|
|
centerX: 100,
|
|
centerY: 80,
|
|
width: 120,
|
|
height: 60,
|
|
rotation: 0,
|
|
}, { zoom: 1 });
|
|
console.log(JSON.stringify({
|
|
ids: geometry.resizeHandles.map(handle => handle.id),
|
|
positions: Object.fromEntries(geometry.resizeHandles.map(handle => [handle.id, [handle.x, handle.y]])),
|
|
corners: geometry.corners,
|
|
pivot: geometry.pivot,
|
|
}));
|
|
"""
|
|
)
|
|
assert result == {
|
|
"ids": ["tl", "t", "tr", "r", "br", "b", "bl", "l"],
|
|
"positions": {
|
|
"tl": [40, 50],
|
|
"t": [100, 50],
|
|
"tr": [160, 50],
|
|
"r": [160, 80],
|
|
"br": [160, 110],
|
|
"b": [100, 110],
|
|
"bl": [40, 110],
|
|
"l": [40, 80],
|
|
},
|
|
"corners": {
|
|
"tl": {"x": 40, "y": 50},
|
|
"tr": {"x": 160, "y": 50},
|
|
"br": {"x": 160, "y": 110},
|
|
"bl": {"x": 40, "y": 110},
|
|
},
|
|
"pivot": {"x": 100, "y": 80},
|
|
}
|
|
|
|
|
|
def test_frame_geometry_rotates_edges_and_corners_around_the_pivot():
|
|
result = run_node(
|
|
"""
|
|
const geometry = transformFrameGeometry({
|
|
centerX: 100,
|
|
centerY: 80,
|
|
width: 120,
|
|
height: 60,
|
|
rotation: 90,
|
|
});
|
|
const rounded = Object.fromEntries(geometry.resizeHandles.map(handle => [
|
|
handle.id,
|
|
[Math.round(handle.x), Math.round(handle.y)],
|
|
]));
|
|
console.log(JSON.stringify(rounded));
|
|
"""
|
|
)
|
|
assert result == {
|
|
"tl": [130, 20],
|
|
"t": [130, 80],
|
|
"tr": [130, 140],
|
|
"r": [100, 140],
|
|
"br": [70, 140],
|
|
"b": [70, 80],
|
|
"bl": [70, 20],
|
|
"l": [100, 20],
|
|
}
|
|
|
|
|
|
def test_hit_testing_uses_every_handle_and_a_larger_invisible_touch_target():
|
|
result = run_node(
|
|
"""
|
|
const geometry = transformFrameGeometry({
|
|
centerX: 100,
|
|
centerY: 80,
|
|
width: 120,
|
|
height: 60,
|
|
rotation: 0,
|
|
}, { zoom: 2 });
|
|
console.log(JSON.stringify({
|
|
rotation: [geometry.rotationHandle.x, geometry.rotationHandle.y],
|
|
edge: hitTestTransformHandle(geometry, 160, 80, { zoom: 2, pointerType: 'mouse' }),
|
|
rotationHit: hitTestTransformHandle(geometry, 100, 38, { zoom: 2, pointerType: 'mouse' }),
|
|
mouseMiss: hitTestTransformHandle(geometry, 168, 80, { zoom: 2, pointerType: 'mouse' }),
|
|
touchHit: hitTestTransformHandle(geometry, 168, 80, { zoom: 2, pointerType: 'touch' }),
|
|
}));
|
|
"""
|
|
)
|
|
assert result == {
|
|
"rotation": [100, 38],
|
|
"edge": "r",
|
|
"rotationHit": "rot",
|
|
"mouseMiss": None,
|
|
"touchHit": "r",
|
|
}
|
|
|
|
|
|
def test_rotated_frame_interior_rejects_empty_axis_aligned_bounding_box_corners():
|
|
result = run_node(
|
|
"""
|
|
const geometry = transformFrameGeometry({
|
|
centerX: 100,
|
|
centerY: 100,
|
|
width: 100,
|
|
height: 100,
|
|
rotation: 45,
|
|
});
|
|
console.log(JSON.stringify({
|
|
center: pointInTransformFrame(geometry, 100, 100),
|
|
visibleTop: pointInTransformFrame(geometry, 100, 40),
|
|
emptyBoundingCorner: pointInTransformFrame(geometry, 35, 35),
|
|
}));
|
|
"""
|
|
)
|
|
assert result == {
|
|
"center": True,
|
|
"visibleTop": True,
|
|
"emptyBoundingCorner": False,
|
|
}
|
|
|
|
|
|
def test_transform_cursors_follow_edge_and_corner_screen_direction():
|
|
script = textwrap.dedent(
|
|
f"""
|
|
import {{cursorForHandle}} from {json.dumps(SNAP_MODULE)};
|
|
console.log(JSON.stringify({{
|
|
right0: cursorForHandle('r', 0),
|
|
top0: cursorForHandle('t', 0),
|
|
corner0: cursorForHandle('br', 0),
|
|
right90: cursorForHandle('r', 90),
|
|
corner90: cursorForHandle('br', 90),
|
|
}}));
|
|
"""
|
|
)
|
|
result = subprocess.run(
|
|
["node", "--input-type=module", "-e", script], cwd=ROOT,
|
|
text=True, capture_output=True, check=True,
|
|
)
|
|
assert json.loads(result.stdout) == {
|
|
"right0": "ew-resize",
|
|
"top0": "ns-resize",
|
|
"corner0": "nwse-resize",
|
|
"right90": "ns-resize",
|
|
"corner90": "nesw-resize",
|
|
}
|
|
|
|
|
|
def test_resize_geometry_keeps_opposite_anchor_at_cardinal_and_arbitrary_angles():
|
|
result = run_node(
|
|
"""
|
|
const angles=[0,45,90,135,37.25];
|
|
const results=[];
|
|
for (const rotation of angles) {
|
|
const frame={centerX:120,centerY:90,width:100,height:60,rotation};
|
|
const before=transformFrameGeometry(frame);
|
|
const start=before.resizeHandles.find(handle=>handle.id==='r');
|
|
const anchor=before.resizeHandles.find(handle=>handle.id==='l');
|
|
const radians=rotation*Math.PI/180;
|
|
const current={x:start.x+20*Math.cos(radians),y:start.y+20*Math.sin(radians)};
|
|
const resized=resizeTransformFrame(frame,'r',start,current);
|
|
const after=transformFrameGeometry({...resized,rotation});
|
|
const nextAnchor=after.resizeHandles.find(handle=>handle.id==='l');
|
|
results.push({
|
|
rotation,width:resized.width,height:resized.height,
|
|
anchorError:Math.hypot(nextAnchor.x-anchor.x,nextAnchor.y-anchor.y),
|
|
});
|
|
}
|
|
console.log(JSON.stringify(results));
|
|
"""
|
|
)
|
|
assert [entry["rotation"] for entry in result] == [0, 45, 90, 135, 37.25]
|
|
for entry in result:
|
|
assert entry["width"] == 120
|
|
assert entry["height"] == 60
|
|
assert entry["anchorError"] < 1e-9
|
|
|
|
|
|
def test_shift_alt_corner_resize_is_centered_and_aspect_locked():
|
|
result = run_node(
|
|
"""
|
|
const frame={centerX:50,centerY:25,width:100,height:50,rotation:0};
|
|
const geometry=transformFrameGeometry(frame);
|
|
const start=geometry.resizeHandles.find(handle=>handle.id==='br');
|
|
const resized=resizeTransformFrame(frame,'br',start,{x:start.x+30,y:start.y+10},{
|
|
centered:true,lockAspect:true,
|
|
});
|
|
console.log(JSON.stringify(resized));
|
|
"""
|
|
)
|
|
assert result == {
|
|
"width": 160,
|
|
"height": 80,
|
|
"centerX": 50,
|
|
"centerY": 25,
|
|
"flipH": False,
|
|
"flipV": False,
|
|
}
|
|
|
|
|
|
def test_skew_drag_shears_the_dragged_edge_only():
|
|
result = run_node(
|
|
"""
|
|
const frame={centerX:0,centerY:0,width:200,height:100,rotation:0};
|
|
const right=skewTransformFrame(frame,'r',{x:0,y:0},{x:0,y:40});
|
|
const top=skewTransformFrame(frame,'t',{x:0,y:0},{x:50,y:0});
|
|
const corner=skewTransformFrame(frame,'br',{x:0,y:0},{x:30,y:20});
|
|
const none=skewTransformFrame(frame,'r',{x:0,y:0},{x:0,y:0});
|
|
console.log(JSON.stringify({right,top,corner,none}));
|
|
"""
|
|
)
|
|
# Right edge dragged down -> skew Y only; top edge dragged right ->
|
|
# skew X only; corner -> both; no drag -> unchanged.
|
|
assert result["right"]["skewX"] == 0
|
|
assert abs(result["right"]["skewY"] - 21.8) < 0.5
|
|
assert abs(result["top"]["skewX"] - (-45.0)) < 0.5
|
|
assert result["top"]["skewY"] == 0
|
|
assert abs(result["corner"]["skewX"] - 30.96) < 0.5
|
|
assert abs(result["corner"]["skewY"] - 11.31) < 0.5
|
|
assert result["none"] == {"skewX": 0, "skewY": 0}
|
|
|
|
|
|
def test_skew_drag_is_relative_to_the_start_and_clamped():
|
|
result = run_node(
|
|
"""
|
|
const frame={centerX:0,centerY:0,width:100,height:100,rotation:0};
|
|
const grown=skewTransformFrame({...frame,height:100},'b',{x:0,y:0},{x:10,y:0},{startSkewX:20,startSkewY:0});
|
|
const huge=skewTransformFrame({...frame,height:1},'b',{x:0,y:0},{x:9000,y:0});
|
|
console.log(JSON.stringify({grown,huge}));
|
|
"""
|
|
)
|
|
assert result["grown"]["skewX"] > 20 # start offset + drag delta
|
|
assert result["huge"]["skewX"] == 85 # clamped below the singular 90°
|
|
|
|
|
|
|
|
def test_distort_drag_moves_only_the_dragged_corner():
|
|
result = run_node(
|
|
"""
|
|
const frame={centerX:0,centerY:0,width:100,height:100,rotation:0};
|
|
const corner=distortTransformFrame(frame,'br',{x:0,y:0},{x:10,y:20});
|
|
console.log(JSON.stringify({corner}));
|
|
"""
|
|
)
|
|
assert result["corner"] == {
|
|
"tl": {"x": 0, "y": 0},
|
|
"tr": {"x": 0, "y": 0},
|
|
"br": {"x": 0.1, "y": 0.2},
|
|
"bl": {"x": 0, "y": 0},
|
|
}
|
|
|
|
|
|
def test_distort_drag_moves_only_the_dragged_edge_corners():
|
|
result = run_node(
|
|
"""
|
|
const frame={centerX:0,centerY:0,width:100,height:100,rotation:0};
|
|
const edge=distortTransformFrame(frame,'t',{x:0,y:0},{x:5,y:-10});
|
|
console.log(JSON.stringify({edge}));
|
|
"""
|
|
)
|
|
assert result["edge"] == {
|
|
"tl": {"x": 0.05, "y": -0.1},
|
|
"tr": {"x": 0.05, "y": -0.1},
|
|
"br": {"x": 0, "y": 0},
|
|
"bl": {"x": 0, "y": 0},
|
|
}
|
|
|
|
|
|
def test_distort_ignores_rotation_and_move_handles():
|
|
result = run_node(
|
|
"""
|
|
const frame={centerX:0,centerY:0,width:100,height:100,rotation:0};
|
|
console.log(JSON.stringify({
|
|
rot: distortTransformFrame(frame,'rot',{x:0,y:0},{x:30,y:0}),
|
|
move: distortTransformFrame(frame,'move',{x:0,y:0},{x:30,y:0}),
|
|
}));
|
|
"""
|
|
)
|
|
assert result["rot"] is None
|
|
assert result["move"] is None
|
|
|
|
|
|
def test_distort_is_clamped_and_relative_to_the_start():
|
|
result = run_node(
|
|
"""
|
|
const frame={centerX:0,centerY:0,width:100,height:100,rotation:0};
|
|
const huge=distortTransformFrame(frame,'br',{x:0,y:0},{x:900,y:0});
|
|
const grown=distortTransformFrame(frame,'br',{x:0,y:0},{x:10,y:0},{startDistort:{tl:{x:0,y:0},tr:{x:0,y:0},br:{x:0.5,y:0},bl:{x:0,y:0}}});
|
|
console.log(JSON.stringify({huge: huge.br.x, grown: grown.br.x}));
|
|
"""
|
|
)
|
|
assert result["huge"] == 4 # clamped to the ±4 frame-normal limit
|
|
assert abs(result["grown"] - 0.6) < 1e-9 # start offset + drag delta
|
|
|
|
|
|
def test_geometry_places_handles_on_the_distorted_quad():
|
|
result = run_node(
|
|
"""
|
|
const geometry=transformFrameGeometry({centerX:0,centerY:0,width:100,height:100,rotation:0,distort:{tl:{x:0,y:0},tr:{x:0,y:0},br:{x:0.1,y:0.2},bl:{x:0,y:0}}},{zoom:1});
|
|
console.log(JSON.stringify({
|
|
corners: geometry.corners,
|
|
handles: Object.fromEntries(geometry.resizeHandles.map(h=>[h.id,[h.x,h.y]])),
|
|
}));
|
|
"""
|
|
)
|
|
assert result["corners"] == {
|
|
"tl": {"x": -50, "y": -50},
|
|
"tr": {"x": 50, "y": -50},
|
|
"br": {"x": 60, "y": 70},
|
|
"bl": {"x": -50, "y": 50},
|
|
}
|
|
assert result["handles"]["br"] == [60, 70]
|
|
assert result["handles"]["t"] == [0, -50]
|
|
assert result["handles"]["r"] == [55, 10]
|
|
assert result["handles"]["b"] == [5, 60]
|
|
assert result["handles"]["l"] == [-50, 0]
|