Import LinuxCNC source tree
This commit is contained in:
1
tests/glcanon/expected
Normal file
1
tests/glcanon/expected
Normal file
@@ -0,0 +1 @@
|
||||
ok
|
||||
12
tests/glcanon/test.sh
Executable file
12
tests/glcanon/test.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# Unit tests for the OpenGL 3.3 core / GLES 3.1 preview renderer shared by
|
||||
# AXIS, the GTK screens (Gremlin/gmoccapy/gscreen/hal_gremlin) and QtVCP.
|
||||
#
|
||||
# Both are GL-free: they exercise the backplot palette and the explicit camera
|
||||
# matrices as plain numpy, so they need neither a GPU nor an X display.
|
||||
set -e
|
||||
|
||||
python3 test_backplot_palette.py >&2
|
||||
python3 test_camera_matrices.py >&2
|
||||
|
||||
echo ok
|
||||
246
tests/glcanon/test_backplot_palette.py
Normal file
246
tests/glcanon/test_backplot_palette.py
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The live backplot's palette: where its entries come from, and that an index
|
||||
once assigned is never reassigned.
|
||||
|
||||
These are the properties a pixel comparison cannot see. A palette rebuilt or
|
||||
renumbered between frames still renders a single frame perfectly; it goes wrong
|
||||
only across frames, because the backplot re-uploads just the changed tail of
|
||||
the ring buffer and every vertex left resident keeps the index it was written
|
||||
with. So the append-only rule is asserted directly here rather than inferred
|
||||
from a picture.
|
||||
|
||||
GL-free; runs anywhere numpy is available:
|
||||
python3 tests/glcanon/test_backplot_palette.py
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
# Loaded by path rather than as rs274.glcanon_bake: the rs274 package __init__
|
||||
# pulls in the compiled gcode extension, which this test does not need.
|
||||
import importlib.util
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"glcanon_bake", os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"..", "..", "lib", "python", "rs274",
|
||||
"glcanon_bake.py"))
|
||||
bake = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(bake)
|
||||
|
||||
|
||||
def point(x, y, z, c, c2=None):
|
||||
"""One ``struct logger_point``."""
|
||||
return struct.pack('<3f4B3f4B', x, y, z, *c,
|
||||
x, y + 1.0, 1.5, *(c2 if c2 is not None else c))
|
||||
|
||||
|
||||
def cats_of(verts):
|
||||
return verts[:, 3].view(np.uint32) >> bake.BACKPLOT_CAT_SHIFT
|
||||
|
||||
|
||||
def linenos_of(verts):
|
||||
return verts[:, 3].view(np.uint32) & ((1 << bake.BACKPLOT_CAT_SHIFT) - 1)
|
||||
|
||||
|
||||
RED = (255, 0, 0, 255)
|
||||
GREEN = (0, 255, 0, 255)
|
||||
BLUE = (0, 0, 255, 255)
|
||||
|
||||
|
||||
class Layout(unittest.TestCase):
|
||||
def test_indexed_layout_is_the_shared_twenty_byte_vertex(self):
|
||||
raw = point(1.0, 2.0, 3.0, RED) + point(4.0, 5.0, 6.0, RED)
|
||||
pal = bake.ColorPalette()
|
||||
v = bake.backplot_vertices(raw, 2, 0, palette=pal)
|
||||
self.assertEqual(v.shape, (2, bake.TRAJ_FLOATS_PER_VERTEX))
|
||||
self.assertEqual(v.dtype, np.float32)
|
||||
self.assertEqual(v.nbytes // 2, 16)
|
||||
np.testing.assert_allclose(v[:, 0:3], [[1, 2, 3], [4, 5, 6]])
|
||||
# The line-number bits are unused: the trail is never picked.
|
||||
np.testing.assert_array_equal(linenos_of(v), [0, 0])
|
||||
|
||||
def test_without_a_palette_the_old_layout_is_returned(self):
|
||||
raw = point(1.0, 2.0, 3.0, RED)
|
||||
v = bake.backplot_vertices(raw, 1, 0)
|
||||
self.assertEqual(v.shape, (1, bake.FLOATS_PER_VERTEX))
|
||||
|
||||
def test_empty_buffer_matches_the_requested_layout(self):
|
||||
self.assertEqual(bake.backplot_vertices(b"", 0, 0).shape,
|
||||
(0, bake.FLOATS_PER_VERTEX))
|
||||
self.assertEqual(
|
||||
bake.backplot_vertices(b"", 0, 0,
|
||||
palette=bake.ColorPalette()).shape,
|
||||
(0, bake.TRAJ_FLOATS_PER_VERTEX))
|
||||
|
||||
|
||||
class PaletteSource(unittest.TestCase):
|
||||
def test_entries_come_from_the_stored_bytes(self):
|
||||
"""Not from the preview's colour table, which holds untruncated floats.
|
||||
|
||||
axis.py stores ``int(component * 255)``. For backplottraverse's 0.30
|
||||
that is 76, and 76/255 is 0.298039 - so a palette built from the table
|
||||
would shift every backplot colour by up to 1/255. Invisible in a
|
||||
screenshot threshold, wrong nonetheless.
|
||||
"""
|
||||
stored = (int(0.30 * 255), int(0.50 * 255), int(0.50 * 255),
|
||||
int(0.25 * 255))
|
||||
self.assertEqual(stored, (76, 127, 127, 63))
|
||||
pal = bake.ColorPalette()
|
||||
bake.backplot_vertices(point(0, 0, 0, stored), 1, 0, palette=pal)
|
||||
entry = pal.entries[0]
|
||||
np.testing.assert_allclose(entry, [c / 255.0 for c in stored])
|
||||
self.assertNotEqual(entry[0], 0.30)
|
||||
self.assertNotEqual(entry[1], 0.50)
|
||||
|
||||
def test_entry_matches_what_the_old_bake_put_on_each_vertex(self):
|
||||
"""The palette entry is exactly the colour the old path drew."""
|
||||
c = (76, 127, 127, 63)
|
||||
raw = point(0, 0, 0, c) + point(1, 1, 1, c)
|
||||
old = bake.backplot_vertices(raw, 2, 0) # per-vertex colours
|
||||
pal = bake.ColorPalette()
|
||||
bake.backplot_vertices(raw, 2, 0, palette=pal)
|
||||
np.testing.assert_array_equal(
|
||||
np.asarray(pal.entries[0], dtype=np.float32), old[0, 3:7])
|
||||
|
||||
def test_first_seen_order(self):
|
||||
raw = point(0, 0, 0, GREEN) + point(1, 0, 0, RED) + point(2, 0, 0, BLUE)
|
||||
pal = bake.ColorPalette()
|
||||
v = bake.backplot_vertices(raw, 3, 0, palette=pal)
|
||||
np.testing.assert_array_equal(cats_of(v), [0, 1, 2])
|
||||
np.testing.assert_allclose(pal.entries,
|
||||
[(0, 1, 0, 1), (1, 0, 0, 1), (0, 0, 1, 1)])
|
||||
|
||||
|
||||
class AppendOnly(unittest.TestCase):
|
||||
"""The one failure mode that yields a wrong picture from correct-looking
|
||||
code: renumbering a colour that resident vertices still refer to."""
|
||||
|
||||
def test_a_new_colour_leaves_earlier_points_untouched(self):
|
||||
pal = bake.ColorPalette()
|
||||
first = point(0, 0, 0, RED) + point(1, 0, 0, RED)
|
||||
before = bake.backplot_vertices(first, 2, 0, palette=pal)
|
||||
entries_before = list(pal.entries)
|
||||
|
||||
# A later frame appends points of a colour never seen before.
|
||||
second = first + point(2, 0, 0, GREEN) + point(3, 0, 0, GREEN)
|
||||
after = bake.backplot_vertices(second, 4, 0, palette=pal)
|
||||
|
||||
np.testing.assert_array_equal(cats_of(after)[:2], cats_of(before))
|
||||
self.assertEqual(pal.entries[:len(entries_before)], entries_before)
|
||||
self.assertEqual(pal.entries[cats_of(after)[2]], (0.0, 1.0, 0.0, 1.0))
|
||||
|
||||
def test_a_colour_that_stops_occurring_keeps_its_index(self):
|
||||
pal = bake.ColorPalette()
|
||||
bake.backplot_vertices(point(0, 0, 0, RED), 1, 0, palette=pal)
|
||||
# The ring has dropped every red point; only green remains.
|
||||
v = bake.backplot_vertices(point(0, 0, 0, GREEN), 1, 0, palette=pal)
|
||||
self.assertEqual(pal.entries[0], (1.0, 0.0, 0.0, 1.0))
|
||||
self.assertEqual(int(cats_of(v)[0]), 1)
|
||||
|
||||
def test_indices_are_stable_across_a_growing_prefix(self):
|
||||
"""Converting a longer prefix must not renumber the shorter one."""
|
||||
pal = bake.ColorPalette()
|
||||
colours = [RED, GREEN, BLUE, RED, GREEN]
|
||||
raw = b"".join(point(i, 0, 0, c) for i, c in enumerate(colours))
|
||||
seen = None
|
||||
for n in range(1, len(colours) + 1):
|
||||
v = bake.backplot_vertices(raw, n, 0, palette=pal)
|
||||
if seen is not None:
|
||||
np.testing.assert_array_equal(cats_of(v)[:len(seen)], seen)
|
||||
seen = cats_of(v)
|
||||
|
||||
|
||||
class IncrementalConversion(unittest.TestCase):
|
||||
"""A trail converted a tail at a time is the trail converted in one pass.
|
||||
|
||||
This is what narrowing the per-frame conversion rests on, and it is not
|
||||
self-evident: indices are assigned in first-seen order *within the
|
||||
converted array*, and a tail sees a different slice than the whole buffer
|
||||
does. It holds only because the palette is append-only and already carries
|
||||
every colour the earlier frames saw - so it is asserted rather than
|
||||
reasoned about.
|
||||
"""
|
||||
|
||||
COLOURS = [RED, GREEN, BLUE, (255, 255, 0, 191), (0, 255, 255, 63)]
|
||||
|
||||
#: the npts the logger reports on successive frames
|
||||
FRAMES = (2, 3, 6, 7, 18, 34, 40)
|
||||
|
||||
def buffer(self, npts):
|
||||
return b"".join(
|
||||
point(i * 0.5, 0, 0, self.COLOURS[(i // 3) % len(self.COLOURS)])
|
||||
for i in range(npts))
|
||||
|
||||
def check(self, is_xyuv):
|
||||
npts = self.FRAMES[-1]
|
||||
raw = self.buffer(npts)
|
||||
vpp = 2 if is_xyuv else 1
|
||||
|
||||
whole_pal = bake.ColorPalette()
|
||||
whole = bake.backplot_vertices(raw, npts, is_xyuv, palette=whole_pal)
|
||||
|
||||
# The caller's rule: start at the last point it already sent, which
|
||||
# the C may still be moving.
|
||||
tail_pal = bake.ColorPalette()
|
||||
built = np.zeros_like(whole)
|
||||
prev = 0
|
||||
for n in self.FRAMES:
|
||||
first_point = max(prev - 1, 0)
|
||||
v = bake.backplot_vertices(raw, n, is_xyuv,
|
||||
first_point=first_point,
|
||||
palette=tail_pal)
|
||||
self.assertEqual(len(v), (n - first_point) * vpp)
|
||||
built[first_point * vpp:first_point * vpp + len(v)] = v
|
||||
prev = n
|
||||
|
||||
np.testing.assert_array_equal(cats_of(built), cats_of(whole))
|
||||
np.testing.assert_array_equal(built, whole)
|
||||
self.assertEqual(tail_pal.entries, whole_pal.entries)
|
||||
|
||||
def test_a_tail_at_a_time_matches_one_pass(self):
|
||||
self.check(0)
|
||||
|
||||
def test_a_tail_at_a_time_matches_one_pass_in_foam(self):
|
||||
self.check(1)
|
||||
|
||||
|
||||
class Foam(unittest.TestCase):
|
||||
def test_both_plane_vertices_share_one_entry(self):
|
||||
"""The C writes ``np.c = np.c2 = c``, so the pair is one colour."""
|
||||
raw = point(0, 0, 0, RED) + point(1, 0, 0, GREEN)
|
||||
pal = bake.ColorPalette()
|
||||
v = bake.backplot_vertices(raw, 2, 1, palette=pal)
|
||||
self.assertEqual(v.shape, (4, bake.TRAJ_FLOATS_PER_VERTEX))
|
||||
cats = cats_of(v)
|
||||
self.assertEqual(int(cats[0]), int(cats[1])) # point 0, both planes
|
||||
self.assertEqual(int(cats[2]), int(cats[3])) # point 1, both planes
|
||||
self.assertNotEqual(int(cats[0]), int(cats[2]))
|
||||
self.assertEqual(len(pal.entries), 2)
|
||||
|
||||
def test_plane_positions_are_interleaved_as_before(self):
|
||||
pal = bake.ColorPalette()
|
||||
v = bake.backplot_vertices(point(1, 2, 3, RED), 1, 1, palette=pal)
|
||||
np.testing.assert_allclose(v[0, 0:3], [1, 2, 3]) # XY plane
|
||||
np.testing.assert_allclose(v[1, 0:3], [1, 3, 1.5]) # UV plane
|
||||
|
||||
|
||||
class Overflow(unittest.TestCase):
|
||||
def test_too_many_colours_falls_back_rather_than_wrapping(self):
|
||||
colours = [(i * 8, 0, 0, 255) for i in range(bake.PALETTE_SIZE + 2)]
|
||||
raw = b"".join(point(i, 0, 0, c) for i, c in enumerate(colours))
|
||||
pal = bake.ColorPalette()
|
||||
v = bake.backplot_vertices(raw, len(colours), 0, palette=pal)
|
||||
self.assertTrue(pal.overflowed)
|
||||
self.assertEqual(v.shape, (len(colours), bake.FLOATS_PER_VERTEX))
|
||||
# Every colour still exact - none dropped, merged or wrapped.
|
||||
np.testing.assert_allclose(v[:, 3], [c[0] / 255.0 for c in colours])
|
||||
|
||||
def test_the_bound_is_not_reached_by_the_logger(self):
|
||||
"""Six motion types into eight slots; the fallback cannot trigger."""
|
||||
self.assertLessEqual(6, bake.PALETTE_SIZE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
144
tests/glcanon/test_camera_matrices.py
Normal file
144
tests/glcanon/test_camera_matrices.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Camera matrices recorded from the pre-refactor GL compatibility path.
|
||||
|
||||
References were captured with Mesa llvmpipe's OpenGL 4.5 compatibility context
|
||||
using an 800x600 viewport, center (1.25, -2.5, 3.75), and extents (4, 6, 8).
|
||||
OpenGL returns column-major matrices, hence the transpose before comparison.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"..", "..", "lib", "python"))
|
||||
|
||||
import glnav
|
||||
|
||||
|
||||
class Navigation(glnav.GlNavBase):
|
||||
def _redraw(self):
|
||||
pass
|
||||
|
||||
def winfo_width(self):
|
||||
return 800
|
||||
|
||||
def winfo_height(self):
|
||||
return 600
|
||||
|
||||
def extents_info(self):
|
||||
return (1.25, -2.5, 3.75), (4.0, 6.0, 8.0)
|
||||
|
||||
def is_lathe(self):
|
||||
return self.lathe
|
||||
|
||||
|
||||
def legacy_matrix(columns):
|
||||
return np.asarray(columns, dtype=np.float64).T
|
||||
|
||||
|
||||
REFERENCES = {
|
||||
"rotate_snap": [[1, 0, 0, 0], [0, -4.3711388e-08, 1, 0],
|
||||
[0, -1, -4.3711388e-08, 0], [0, 1.25, 6.25, 1]],
|
||||
"translate_zoom_boost": [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0],
|
||||
[0.1273418665, 0.0955063999, 0, 1]],
|
||||
"view_x": [[-4.3711388e-08, 0, 1, 0], [1, -4.3711388e-08, 4.3711388e-08, 0],
|
||||
[4.3711388e-08, 1, 1.9106855e-15, 0], [2.49999976, -3.75, -1.24999988, 1]],
|
||||
"view_y": [[-4.3711388e-08, -1, 4.3711388e-08, 0], [0, -4.3711388e-08, -1, 0],
|
||||
[1, -4.3711388e-08, 1.9106855e-15, 0], [-3.75, 1.25, -2.5, 1]],
|
||||
"view_y2": [[-4.3711388e-08, 1, 4.3711388e-08, 0], [0, -4.3711388e-08, 1, 0],
|
||||
[1, 4.3711388e-08, 1.9106855e-15, 0], [-3.75, -1.25000024, 2.5, 1]],
|
||||
"view_z": [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [-1.25, 2.5, -3.75, 1]],
|
||||
"view_z2": [[-4.3711388e-08, -1, 0, 0], [1, -4.3711388e-08, 0, 0],
|
||||
[0, 0, 1, 0], [2.5, 1.24999988, -3.75, 1]],
|
||||
"view_p": [[.9063076973, -.2113092095, .3659983277, 0],
|
||||
[.4226184487, .4531538188, -.7848855257, 0],
|
||||
[0, .8660254478, .4999999702, 0],
|
||||
[-.0763385296, -1.850574255, -4.29471159, 1]],
|
||||
}
|
||||
|
||||
|
||||
class ExplicitCameraMatrixTest(unittest.TestCase):
|
||||
def nav(self, lathe=False):
|
||||
result = Navigation()
|
||||
result.lathe = lathe
|
||||
return result
|
||||
|
||||
def assert_matrix(self, name, nav):
|
||||
np.testing.assert_allclose(nav.get_modelview_matrix(), legacy_matrix(REFERENCES[name]),
|
||||
rtol=0, atol=3e-6)
|
||||
|
||||
def test_rotation_snaps_to_ninety_degrees(self):
|
||||
nav = self.nav()
|
||||
nav.set_centerpoint(1.25, -2.5, 3.75)
|
||||
nav.lat = 89
|
||||
nav.recordMouse(0, 0)
|
||||
nav.rotate(4, 2) # 90° and 2° both snap to legacy 90°/0° angles.
|
||||
self.assert_matrix("rotate_snap", nav)
|
||||
|
||||
def test_translate_uses_projection_and_zoom_boost(self):
|
||||
nav = self.nav()
|
||||
nav.distance = 2.0
|
||||
nav.recordMouse(10, 20)
|
||||
nav.translate(30, 5)
|
||||
self.assert_matrix("translate_zoom_boost", nav)
|
||||
|
||||
def test_zoom_changes_distance_but_not_modelview(self):
|
||||
nav = self.nav()
|
||||
original = nav.get_modelview_matrix()
|
||||
nav.zoomin()
|
||||
nav.zoomout()
|
||||
np.testing.assert_allclose(nav.get_modelview_matrix(), original)
|
||||
self.assertAlmostEqual(nav.distance, 9.98)
|
||||
|
||||
def test_view_presets_match_legacy(self):
|
||||
for name, lathe in (("x", False), ("y", True), ("y2", False),
|
||||
("z", False), ("z2", False), ("p", False)):
|
||||
with self.subTest(view=name):
|
||||
nav = self.nav(lathe)
|
||||
getattr(nav, "set_view_" + name)()
|
||||
self.assert_matrix("view_" + name, nav)
|
||||
|
||||
def test_translate_modelview_matches_manual_composition(self):
|
||||
nav = self.nav()
|
||||
nav.set_view_p()
|
||||
before = nav.get_modelview_matrix()
|
||||
nav.translate_modelview(3.5, -1.75, 0.25)
|
||||
expected = glnav.multiply(before, glnav.translation_matrix(3.5, -1.75, 0.25))
|
||||
np.testing.assert_allclose(nav.get_modelview_matrix(), expected)
|
||||
|
||||
def test_translate_modelview_offset_survives_plasmac_sequence(self):
|
||||
# The plasmac table view: top view, offset to the machine table centre,
|
||||
# then refit the eyepoint. The offset has to survive the refit.
|
||||
xcenter, ycenter = 12.0, -7.5
|
||||
nav = self.nav()
|
||||
nav.set_view_z()
|
||||
after_preset = nav.get_modelview_matrix()
|
||||
nav.translate_modelview(-xcenter, -ycenter, 0)
|
||||
nav.set_eyepoint_from_extents(4.0, 6.0)
|
||||
expected = glnav.multiply(after_preset,
|
||||
glnav.translation_matrix(-xcenter, -ycenter, 0))
|
||||
np.testing.assert_allclose(nav.get_modelview_matrix(), expected)
|
||||
|
||||
def test_translate_modelview_leaves_navigation_state_alone(self):
|
||||
nav = self.nav()
|
||||
nav.set_view_p()
|
||||
nav.recordMouse(10, 20)
|
||||
nav.translate(30, 5)
|
||||
state = (nav.lat, nav.lon, nav.distance, nav._totalx, nav._totaly)
|
||||
nav.translate_modelview(-4.0, 2.5, 0)
|
||||
self.assertEqual((nav.lat, nav.lon, nav.distance, nav._totalx, nav._totaly), state)
|
||||
|
||||
def test_ortho_projection_matches_legacy_scale(self):
|
||||
nav = self.nav()
|
||||
nav.distance = 2.0
|
||||
k = 2.0 ** .55555
|
||||
expected = glnav.multiply(
|
||||
glnav.ortho_matrix(-k, k, -k * 600 / 800, k * 600 / 800, -1000, 1000),
|
||||
glnav.translation_matrix(0, 0, -1))
|
||||
np.testing.assert_allclose(nav.get_projection_matrix(800, 600), expected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user