modify 3d view to sync / identical to 2d view
This commit is contained in:
@@ -27,6 +27,7 @@ import pytest
|
||||
from orisys_app_shared.scene3d import synthetic
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
ColorScale,
|
||||
DisplayField,
|
||||
DisplaySettings,
|
||||
FieldDescriptor,
|
||||
FrameIdentity,
|
||||
@@ -44,6 +45,48 @@ pytestmark = pytest.mark.skipif(
|
||||
BACKGROUND_RGB = np.array([15, 18, 23])
|
||||
|
||||
|
||||
def _field(assets, values, valid=None, descriptor=None) -> DisplayField:
|
||||
"""A display field on the scene's own domain, from a flat or gridded array.
|
||||
|
||||
The surface's colour and validity now come from the *field*, not from the vertices,
|
||||
so a test that wants a particular picture builds one of these and hands it to the
|
||||
widget. ``values`` is reshaped to the scene's ``[H,W]`` domain — which is what the
|
||||
2D canvas would paint — and the fragment reads it through the mesh's UVs.
|
||||
"""
|
||||
shape = (int(assets.shape_hw[0]), int(assets.shape_hw[1]))
|
||||
scalar = np.asarray(values, dtype=np.float32).reshape(shape)
|
||||
mask = (
|
||||
np.ones(shape, dtype=bool)
|
||||
if valid is None
|
||||
else np.asarray(valid, dtype=bool).reshape(shape)
|
||||
)
|
||||
return DisplayField(
|
||||
domain_id=assets.field_domain_id,
|
||||
shape_hw=shape,
|
||||
scalar=freeze(np.array(scalar, dtype=np.float32), dtype=np.float32),
|
||||
valid=freeze(mask, dtype=np.bool_),
|
||||
descriptor=(
|
||||
descriptor
|
||||
or FieldDescriptor("flow_magnitude", "px", "approximate", False)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _frame(assets, values, valid=None, *, frame_id=5, positions=None, normals=None,
|
||||
descriptor=None, deformed=False) -> RenderFrame:
|
||||
part = assets.sensing_part()
|
||||
return RenderFrame(
|
||||
identity=FrameIdentity("poc", 1, frame_id, 0.5, generation=1),
|
||||
asset_hash=assets.asset_hash,
|
||||
geometry_id=assets.geometry_id,
|
||||
sensing_part_id=part.part_id,
|
||||
positions_sensor_m=part.positions_sensor_m if positions is None else positions,
|
||||
normals_sensor=part.normals_sensor if normals is None else normals,
|
||||
field=_field(assets, values, valid, descriptor),
|
||||
deformed=deformed,
|
||||
)
|
||||
|
||||
|
||||
def _pump(qt_app, widget, predicate, *, timeout_s: float = 5.0) -> bool:
|
||||
"""Let Qt paint until ``predicate`` holds. Returns whether it did."""
|
||||
import time
|
||||
@@ -123,6 +166,13 @@ def _plate_is_grey(pixels: np.ndarray) -> float:
|
||||
return float(np.abs(drawn[:, 0] - drawn[:, 1]).mean())
|
||||
|
||||
|
||||
def _plate_mean_rgb(pixels: np.ndarray) -> np.ndarray:
|
||||
"""The mean RGB of the drawn pixels: the plate's own colour, on a flat surface."""
|
||||
drawn = _drawn(pixels)
|
||||
assert drawn.size, "nothing was drawn"
|
||||
return drawn.mean(axis=0)
|
||||
|
||||
|
||||
def test_the_capabilities_come_from_a_real_context(gl_widget):
|
||||
widget, _, _ = gl_widget
|
||||
capabilities = widget.capabilities()
|
||||
@@ -157,33 +207,30 @@ def test_both_parts_are_drawn_and_the_housing_can_be_hidden(gl_widget, qt_app):
|
||||
def test_the_rest_frame_is_drawn_neutral_not_as_measurement_zero(gl_widget, qt_app):
|
||||
"""No measurement yet: the plate must not be coloured as if it had one.
|
||||
|
||||
Checked as a difference: the neutral path is grey (r≈g), the colour map's low
|
||||
end is not. Counting "grey pixels" over the whole frame would just be counting
|
||||
the background.
|
||||
Asserted as a *colour identity*, not as a hue difference: the valid path must draw
|
||||
the lookup table's own colour for the value it was given, and the neutral path must
|
||||
draw the neutral colour. (The old form of this test compared mean ``|r-g|``, which
|
||||
only worked because the 3D pane used a purple ramp whose low end is far from grey.
|
||||
With one ramp serving both views the low end is the theme's own navy, where ``r≈g``,
|
||||
so a hue test would have had to be loosened until it stopped meaning anything. A
|
||||
colour-identity test is both stricter and independent of which ramp is in use.)
|
||||
"""
|
||||
from orisys_studio.scene3d.gl_backend import INVALID_COLOR, LUT_SIZE, lut_rgba
|
||||
|
||||
widget, _, _ = gl_widget
|
||||
assets = synthetic.poc_scene()
|
||||
count = int(assets.sensing_part().positions_sensor_m.shape[0])
|
||||
widget.set_settings(DisplaySettings(housing_visible=False))
|
||||
|
||||
rest_grey = _plate_is_grey(_render(widget, qt_app))
|
||||
rest_rgb = _plate_mean_rgb(_render(widget, qt_app))
|
||||
neutral_8bit = np.array(INVALID_COLOR[:3]) * 255.0
|
||||
|
||||
frame = RenderFrame(
|
||||
identity=FrameIdentity("poc", 1, 5, 0.5, generation=1),
|
||||
asset_hash=assets.asset_hash,
|
||||
geometry_id=assets.geometry_id,
|
||||
sensing_part_id=assets.sensing_part().part_id,
|
||||
positions_sensor_m=assets.sensing_part().positions_sensor_m,
|
||||
normals_sensor=assets.sensing_part().normals_sensor,
|
||||
scalar=freeze(np.full(count, 0.05, dtype=np.float32), dtype=np.float32),
|
||||
valid=freeze(np.ones(count, dtype=np.bool_), dtype=np.bool_),
|
||||
scalar_descriptor=FieldDescriptor("flow_magnitude", "px", "approximate", False),
|
||||
)
|
||||
value = 0.05
|
||||
frame = _frame(assets, np.full(assets.shape_hw, value, dtype=np.float32))
|
||||
widget.set_settings(
|
||||
DisplaySettings(
|
||||
housing_visible=False,
|
||||
color_scale=ColorScale(
|
||||
descriptor=frame.scalar_descriptor,
|
||||
descriptor=frame.field.descriptor,
|
||||
lower=0.0,
|
||||
upper=1.0,
|
||||
lut_id="sequential",
|
||||
@@ -191,12 +238,27 @@ def test_the_rest_frame_is_drawn_neutral_not_as_measurement_zero(gl_widget, qt_a
|
||||
)
|
||||
)
|
||||
widget.present(frame)
|
||||
measured_grey = _plate_is_grey(_render(widget, qt_app))
|
||||
measured_rgb = _plate_mean_rgb(_render(widget, qt_app))
|
||||
|
||||
assert rest_grey < 6.0, f"the unmeasured surface is not neutral (r-g={rest_grey:.1f})"
|
||||
assert measured_grey > rest_grey + 10.0, (
|
||||
"the colour map made no difference: the invalid path is not being taken "
|
||||
f"(rest r-g={rest_grey:.1f}, measured r-g={measured_grey:.1f})"
|
||||
# Where the fragment shader samples the table: ``vec2(t, 0.5)`` on a 256x1 texture
|
||||
# with linear filtering, i.e. ``t * 256 - 0.5`` clamped into the table.
|
||||
table = lut_rgba("sequential")[:, 0, :3].astype(float)
|
||||
position = float(np.clip(value * LUT_SIZE - 0.5, 0.0, LUT_SIZE - 1.0))
|
||||
low = int(np.floor(position))
|
||||
high = min(low + 1, LUT_SIZE - 1)
|
||||
fraction = position - low
|
||||
expected = (1.0 - fraction) * table[low] + fraction * table[high]
|
||||
|
||||
assert np.abs(rest_rgb - neutral_8bit).max() < 6.0, (
|
||||
f"the unmeasured surface is not the neutral colour: {rest_rgb} vs {neutral_8bit}"
|
||||
)
|
||||
assert np.abs(measured_rgb - expected).max() < 8.0, (
|
||||
"the colour map's own colour did not reach the plate: "
|
||||
f"drawn {measured_rgb}, the table gives {expected} for {value} — the valid path "
|
||||
"is not being taken, or the pane is not using the shared ramp"
|
||||
)
|
||||
assert np.abs(expected - neutral_8bit).max() > 10.0, (
|
||||
"this value is too close to the neutral colour to make the point; pick another"
|
||||
)
|
||||
widget.set_settings(DisplaySettings(housing_visible=True))
|
||||
|
||||
@@ -207,21 +269,18 @@ def test_a_frame_updates_only_the_sensing_buffers(gl_widget, qt_app):
|
||||
count = int(assets.sensing_part().positions_sensor_m.shape[0])
|
||||
positions = np.asarray(assets.sensing_part().positions_sensor_m).copy()
|
||||
positions[:, 2] = 0.004 # a visible bump
|
||||
frame = RenderFrame(
|
||||
identity=FrameIdentity("poc", 1, 6, 0.6, generation=1),
|
||||
asset_hash=assets.asset_hash,
|
||||
geometry_id=assets.geometry_id,
|
||||
sensing_part_id=assets.sensing_part().part_id,
|
||||
positions_sensor_m=freeze(positions, dtype=np.float32),
|
||||
normals_sensor=assets.sensing_part().normals_sensor,
|
||||
scalar=freeze(np.linspace(0.0, 1.0, count).astype(np.float32), dtype=np.float32),
|
||||
valid=freeze(np.ones(count, dtype=np.bool_), dtype=np.bool_),
|
||||
scalar_descriptor=FieldDescriptor("flow_magnitude", "px", "approximate", False),
|
||||
frame = _frame(
|
||||
assets,
|
||||
np.linspace(0.0, 1.0, count).reshape(assets.shape_hw),
|
||||
frame_id=6,
|
||||
positions=freeze(positions, dtype=np.float32),
|
||||
deformed=True,
|
||||
)
|
||||
widget.set_settings(
|
||||
DisplaySettings(
|
||||
color_scale=ColorScale(
|
||||
descriptor=frame.scalar_descriptor, lower=0.0, upper=1.0, lut_id="sequential"
|
||||
descriptor=frame.field.descriptor, lower=0.0, upper=1.0,
|
||||
lut_id="sequential",
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -242,21 +301,13 @@ def _buffer_names(renderer, part_id: str) -> tuple[int, int, int]:
|
||||
def test_a_frame_from_another_geometry_is_refused_without_losing_the_view(gl_widget, qt_app):
|
||||
"""A bad frame is refused; the scene stays usable and stops showing it."""
|
||||
widget, _, failures = gl_widget
|
||||
count = 9
|
||||
before = widget.rejected_frames
|
||||
frame = RenderFrame(
|
||||
identity=FrameIdentity("poc", 1, 2, 0.2, generation=1),
|
||||
asset_hash="other",
|
||||
geometry_id="some-other-geometry",
|
||||
sensing_part_id="sensing_surface",
|
||||
positions_sensor_m=freeze(np.zeros((count, 3), dtype=np.float32), dtype=np.float32),
|
||||
normals_sensor=freeze(
|
||||
np.tile([0.0, 0.0, 1.0], (count, 1)).astype(np.float32), dtype=np.float32
|
||||
),
|
||||
scalar=freeze(np.zeros(count, dtype=np.float32), dtype=np.float32),
|
||||
valid=freeze(np.zeros(count, dtype=np.bool_), dtype=np.bool_),
|
||||
scalar_descriptor=FieldDescriptor("flow_magnitude", "px", "approximate", False),
|
||||
frame = _frame(
|
||||
synthetic.poc_scene(), np.zeros(synthetic.poc_scene().shape_hw, dtype=np.float32),
|
||||
frame_id=2,
|
||||
)
|
||||
object.__setattr__(frame, "asset_hash", "other")
|
||||
object.__setattr__(frame, "geometry_id", "some-other-geometry")
|
||||
widget.present(frame)
|
||||
assert _drawn(_render(widget, qt_app)).size, "the view died with a refused frame"
|
||||
assert widget.rejected_frames == before + 1
|
||||
@@ -525,7 +576,6 @@ def test_an_automatic_colour_scale_follows_the_frame(assembled_view, qt_app):
|
||||
widget = view.gl_widget
|
||||
assets_frame = view.assets
|
||||
assert assets_frame is not None
|
||||
count = int(assets_frame.sensing_part().positions_sensor_m.shape[0])
|
||||
|
||||
def frame_with(values: np.ndarray) -> RenderFrame:
|
||||
return RenderFrame(
|
||||
@@ -535,9 +585,7 @@ def test_an_automatic_colour_scale_follows_the_frame(assembled_view, qt_app):
|
||||
sensing_part_id=assets_frame.sensing_part().part_id,
|
||||
positions_sensor_m=assets_frame.sensing_part().positions_sensor_m,
|
||||
normals_sensor=assets_frame.sensing_part().normals_sensor,
|
||||
scalar=freeze(np.asarray(values, dtype=np.float32), dtype=np.float32),
|
||||
valid=freeze(np.ones(count, dtype=np.bool_), dtype=np.bool_),
|
||||
scalar_descriptor=FieldDescriptor("flow_magnitude", "px", "approximate", False),
|
||||
field=_field(assets_frame, values),
|
||||
)
|
||||
|
||||
descriptor = FieldDescriptor("flow_magnitude", "px", "approximate", False)
|
||||
@@ -553,7 +601,7 @@ def test_an_automatic_colour_scale_follows_the_frame(assembled_view, qt_app):
|
||||
descriptor=descriptor, lower=0.0, upper=1000.0, lut_id="sequential", mode="fixed"
|
||||
),
|
||||
)
|
||||
ramp = np.linspace(3.0, 7.0, count)
|
||||
ramp = np.linspace(3.0, 7.0, int(assets_frame.shape_hw[0]) * int(assets_frame.shape_hw[1]))
|
||||
|
||||
widget.set_settings(fixed_wide)
|
||||
widget.present(frame_with(ramp))
|
||||
|
||||
Reference in New Issue
Block a user