def test_add_item_to_string_list_widget_causes_container_to_relayout( self) -> None: # ugly type casting from nion.ui import Widgets ui = TestUI.UserInterface() widget = Widgets.StringListWidget(ui) with contextlib.closing(widget): canvas_item = typing.cast( CanvasItem.CanvasItemComposition, typing.cast( UserInterface.CanvasWidget, typing.cast( UserInterface.BoxWidget, widget.content_widget).children[0]).canvas_item) canvas_item.update_layout(Geometry.IntPoint(x=0, y=0), Geometry.IntSize(width=300, height=200), immediate=True) scroll_area_canvas_item = typing.cast( CanvasItem.ScrollAreaCanvasItem, typing.cast(CanvasItem.CanvasItemComposition, canvas_item.canvas_items[0]).canvas_items[0]) canvas_item.layout_immediate( Geometry.IntSize(width=300, height=200)) # check assumptions scroll_canvas_rect = scroll_area_canvas_item.canvas_rect or Geometry.IntRect.empty_rect( ) scroll_content = scroll_area_canvas_item.content assert scroll_content self.assertEqual(scroll_canvas_rect.height, 200) scroll_content_rect = scroll_content.canvas_rect or Geometry.IntRect.empty_rect( ) self.assertEqual(scroll_content_rect.height, 0) # add item self.assertFalse(canvas_item._needs_layout_for_testing) widget.items = ["abc"] # self.assertTrue(canvas_item._needs_layout_for_testing) # check that column was laid out again canvas_item.layout_immediate(Geometry.IntSize(width=300, height=200), force=False) scroll_canvas_rect = scroll_area_canvas_item.canvas_rect or Geometry.IntRect.empty_rect( ) scroll_content = scroll_area_canvas_item.content assert scroll_content scroll_content_rect = scroll_content.canvas_rect or Geometry.IntRect.empty_rect( ) self.assertEqual(scroll_canvas_rect.height, 200) self.assertEqual(scroll_content_rect.height, 20)
def setup_camera_hardware_source( self, stem_controller: stem_controller.STEMController, camera_exposure: float, is_eels: bool) -> HardwareSource.HardwareSource: instrument = typing.cast(InstrumentDevice.Instrument, stem_controller) camera_id = "usim_ronchigram_camera" if not is_eels else "usim_eels_camera" camera_type = "ronchigram" if not is_eels else "eels" camera_name = "uSim Camera" camera_settings = CameraDevice.CameraSettings(camera_id) camera_device = CameraDevice.Camera(camera_id, camera_type, camera_name, instrument) if getattr(camera_device, "camera_version", 2) == 3: camera_hardware_source = camera_base.CameraHardwareSource3( "usim_stem_controller", camera_device, camera_settings, None, None) else: camera_hardware_source = camera_base.CameraHardwareSource2( "usim_stem_controller", camera_device, camera_settings, None, None) if is_eels: camera_hardware_source.features["is_eels_camera"] = True camera_hardware_source.add_channel_processor( 0, HardwareSource.SumProcessor( Geometry.FloatRect(Geometry.FloatPoint(0.25, 0.0), Geometry.FloatSize(0.5, 1.0)))) camera_hardware_source.set_frame_parameters( 0, camera_base.CameraFrameParameters({ "exposure_ms": camera_exposure * 1000, "binning": 2 })) camera_hardware_source.set_frame_parameters( 1, camera_base.CameraFrameParameters({ "exposure_ms": camera_exposure * 1000, "binning": 2 })) camera_hardware_source.set_frame_parameters( 2, camera_base.CameraFrameParameters({ "exposure_ms": camera_exposure * 1000 * 2, "binning": 1 })) camera_hardware_source.set_selected_profile_index(0) return camera_hardware_source
def __init__(self, ui, text: str): super().__init__(ui.create_column_widget()) self.on_button_clicked = None font = "normal 11px serif" font_metrics = ui.get_font_metrics(font, text) text_button_canvas_item = TextButtonCanvasItem(text) text_button_canvas_item.sizing.set_fixed_size( Geometry.IntSize(height=font_metrics.height + 6, width=font_metrics.width + 6)) def button_clicked(): if callable(self.on_button_clicked): self.on_button_clicked() text_button_canvas_item.on_button_clicked = button_clicked text_button_canvas_widget = ui.create_canvas_widget(properties={ "height": 20, "width": 20 }) text_button_canvas_widget.canvas_item.add_canvas_item( text_button_canvas_item) # ugh. this is a partially working stop-gap when a canvas item is in a widget it will not get mouse exited reliably text_button_canvas_widget.on_mouse_exited = text_button_canvas_item.root_container.canvas_widget.on_mouse_exited self.content_widget.add(text_button_canvas_widget)
def test_layout_size_maintains_height_with_no_items_when_not_wrapped(self) -> None: selection = Selection.IndexedSelection() delegate = GridCanvasItemDelegate(0) canvas_item = GridCanvasItem.GridCanvasItem(delegate, selection, wrap=False) canvas_item.update_layout(Geometry.IntPoint(), Geometry.IntSize.make((40, 500))) canvas_bounds = canvas_item.canvas_bounds or Geometry.IntRect.empty_rect() self.assertEqual(canvas_bounds.height, 40)
def test_data_item_display_thumbnail_source_produces_data_item_mime_data( self): with TestContext.create_memory_context() as test_context: document_controller = test_context.create_document_controller() document_model = document_controller.document_model data_item = DataItem.DataItem(numpy.random.randn(8, 8)) document_model.append_data_item(data_item) display_item = document_model.get_display_item_for_data_item( data_item) display_item.display_type = "image" thumbnail_source = DataItemThumbnailWidget.DataItemThumbnailSource( document_controller.ui) finished = threading.Event() def thumbnail_data_changed(data): finished.set() thumbnail_source.on_thumbnail_data_changed = thumbnail_data_changed thumbnail_source.set_display_item(display_item) finished.wait(1.0) finished.clear() finished.wait(1.0) mime_data = document_controller.ui.create_mime_data() valid, thumbnail = thumbnail_source.populate_mime_data_for_drag( mime_data, Geometry.IntSize(64, 64)) self.assertTrue(valid) self.assertIsNotNone(thumbnail) self.assertTrue( mime_data.has_format(MimeTypes.DISPLAY_ITEM_MIME_TYPE))
def intersects(self, offset_m: Geometry.FloatPoint, fov_nm: Geometry.FloatSize, center_nm: Geometry.FloatPoint, probe_position: Geometry.FloatPoint) -> bool: scan_rect_m = self.get_scan_rect_m(offset_m, fov_nm, center_nm) feature_rect_m = self.get_feature_rect_m() probe_position_m = Geometry.FloatPoint(y=probe_position.y * scan_rect_m.height + scan_rect_m.top, x=probe_position.x * scan_rect_m.width + scan_rect_m.left) return scan_rect_m.intersects_rect(feature_rect_m) and feature_rect_m.contains_point(probe_position_m)
def __acquisition_thread(self): while True: if self.__cancel: # case where exposure was canceled. break self.__thread_event.wait() self.__thread_event.clear() if self.__cancel: break while (self.__is_playing or self.__is_acquiring) and not self.__cancel: start = time.time() readout_area = self.readout_area binning_shape = Geometry.IntSize( self.__binning, self.__binning if self.__symmetric_binning else 1) xdata = self.__instrument.get_camera_data( self.camera_type, Geometry.IntRect.from_tlbr(*readout_area), binning_shape, self.__exposure) self.__acquired_one_event.set() elapsed = time.time() - start wait_s = max(self.__exposure - elapsed, 0) if not self.__thread_event.wait(wait_s): # thread event was not triggered during wait; signal that we have data xdata._set_timestamp(datetime.datetime.utcnow()) self.__xdata_buffer = xdata self.__has_data_event.set() self.__instrument.trigger_camera_frame() else: # thread event was triggered during wait; continue loop self.__has_data_event.clear() self.__thread_event.clear()
def __init__(self, ui: UserInterface.UserInterface, text: str) -> None: column_widget = ui.create_column_widget() super().__init__(column_widget) self.on_button_clicked: typing.Optional[typing.Callable[[], None]] = None font = "normal 11px serif" font_metrics = ui.get_font_metrics(font, text) text_button_canvas_item = TextButtonCanvasItem(text) text_button_canvas_item.update_sizing( text_button_canvas_item.sizing.with_fixed_size( Geometry.IntSize(height=font_metrics.height + 6, width=font_metrics.width + 6))) def button_clicked() -> None: if callable(self.on_button_clicked): self.on_button_clicked() text_button_canvas_item.on_button_clicked = button_clicked text_button_canvas_widget = ui.create_canvas_widget(properties={ "height": 20, "width": 20 }) text_button_canvas_widget.canvas_item.add_canvas_item( text_button_canvas_item) # ugh. this is a partially working stop-gap when a canvas item is in a widget it will not get mouse exited reliably root_container = text_button_canvas_item.root_container if root_container: text_button_canvas_widget.on_mouse_exited = root_container.canvas_widget.on_mouse_exited column_widget.add(text_button_canvas_widget)
def test_display_data_panel_reuses_existing_display(self): with create_memory_profile_context() as profile_context: document_model = DocumentModel.DocumentModel(profile=profile_context.create_profile()) document_controller = self.app.create_document_controller(document_model, "library") with contextlib.closing(document_controller): # configure data item data_item = DataItem.DataItem(numpy.arange(64).reshape(8, 8)) document_model.append_data_item(data_item) # configure workspace d = {"type": "splitter", "orientation": "vertical", "splits": [0.5, 0.5], "children": [ {"type": "image", "uuid": "0569ca31-afd7-48bd-ad54-5e2bb9f21102", "identifier": "a", "selected": True}, {"type": "image", "uuid": "acd77f9f-2f6f-4fbf-af5e-94330b73b997", "identifier": "b"}]} workspace_2x1 = document_controller.workspace_controller.new_workspace("2x1", d) document_controller.workspace_controller.change_workspace(workspace_2x1) root_canvas_item = document_controller.workspace_controller.image_row.children[0]._root_canvas_item() root_canvas_item.layout_immediate(Geometry.IntSize(width=640, height=480)) self.assertIsNone(document_controller.workspace_controller.display_panels[0].data_item) self.assertIsNone(document_controller.workspace_controller.display_panels[1].data_item) # test display_data_item api = Facade.get_api("~1.0", "~1.0") library = api.library document_controller_ref = api.application.document_controllers[0] data_item_ref = library.data_items[0] # display data item and verify it is displayed display_panal_ref = document_controller_ref.display_data_item(data_item_ref) self.assertEqual(document_controller.workspace_controller.display_panels[0].data_item, data_item_ref._data_item) self.assertIsNone(document_controller.workspace_controller.display_panels[1].data_item) self.assertEqual(document_controller.workspace_controller.display_panels[0], display_panal_ref._display_panel) # display data item again and verify it is displayed only once display_panal_ref = document_controller_ref.display_data_item(data_item_ref) self.assertEqual(document_controller.workspace_controller.display_panels[0].data_item, data_item_ref._data_item) self.assertIsNone(document_controller.workspace_controller.display_panels[1].data_item) self.assertEqual(document_controller.workspace_controller.display_panels[0], display_panal_ref._display_panel)
def test_data_item_display_thumbnail_source_produces_data_item_mime_data( self): app = Application.Application(TestUI.UserInterface(), set_global=False) document_model = DocumentModel.DocumentModel() document_controller = DocumentController.DocumentController( app.ui, document_model, workspace_id="library") with contextlib.closing(document_controller): data_item = DataItem.DataItem(numpy.random.randn(8, 8)) document_model.append_data_item(data_item) display_item = document_model.get_display_item_for_data_item( data_item) display_item.display_type = "image" thumbnail_source = DataItemThumbnailWidget.DataItemThumbnailSource( app.ui) finished = threading.Event() def thumbnail_data_changed(data): finished.set() thumbnail_source.on_thumbnail_data_changed = thumbnail_data_changed thumbnail_source.set_display_item(display_item) finished.wait(1.0) finished.clear() finished.wait(1.0) mime_data = app.ui.create_mime_data() valid, thumbnail = thumbnail_source.populate_mime_data_for_drag( mime_data, Geometry.IntSize(64, 64)) self.assertTrue(valid) self.assertIsNotNone(thumbnail) self.assertTrue( mime_data.has_format(MimeTypes.DISPLAY_ITEM_MIME_TYPE))
def wheel_changed(self, x, y, dx, dy, is_horizontal): dy = dy if not is_horizontal else 0.0 new_canvas_origin = Geometry.IntPoint.make( self.canvas_origin) + Geometry.IntPoint(x=0, y=dy) self.update_layout(new_canvas_origin, self.canvas_size) self.update() return True
def test_setting_and_getting_attribute_values_and_2D_values_works( self) -> None: instrument = InstrumentDevice.Instrument("usim_stem_controller") C12 = Geometry.FloatPoint(x=-1e-5, y=-3e-5) instrument.SetVal("C12.x", C12.x) instrument.SetVal("C12.y", C12.y) self.assertAlmostEqual(C12.x, instrument.GetVal("C12.x")) self.assertAlmostEqual(C12.x, instrument.GetVal2D("C12").x) self.assertAlmostEqual(C12.y, instrument.GetVal("C12.y")) self.assertAlmostEqual(C12.y, instrument.GetVal2D("C12").y) C12 = Geometry.FloatPoint(y=-2e5, x=1e5) instrument.SetVal2D("C12", C12) self.assertAlmostEqual(C12.x, instrument.GetVal("C12.x")) self.assertAlmostEqual(C12.x, instrument.GetVal2D("C12").x) self.assertAlmostEqual(C12.y, instrument.GetVal("C12.y")) self.assertAlmostEqual(C12.y, instrument.GetVal2D("C12").y)
def test_ticker_produces_unique_labels(self): pairs = ((1, 4), (.1, .4), (1E12, 1.000062E12), (1E-18, 1.000062E-18), (-4, -1), (-10000.001, -10000.02), (1E8 - 0.002, 1E8 + 0.002), (0, 1E8 + 0.002)) for logarithmic in (False, True): for l, h in pairs: if not logarithmic or (l > 0 and h > 0): with self.subTest(l=l, h=h, logarithmic=logarithmic): if logarithmic: ticker = Geometry.LogTicker( math.log10(l), math.log10(h)) else: ticker = Geometry.LinearTicker(l, h) self.assertEqual(len(set(ticker.labels)), len(ticker.labels))
def _repaint_visible(self, drawing_context, visible_rect): canvas_size = self.canvas_size if self.__delegate and canvas_size.height > 0 and canvas_size.width > 0: item_size = self.__calculate_item_size(canvas_size) items = self.__delegate.items if self.__delegate else list() item_count = len(items) items_per_row = max( 1, int(canvas_size.width / item_size.width) if self.wrap else item_count) items_per_column = max( 1, int(canvas_size.height / item_size.height) if self.wrap else item_count) with drawing_context.saver(): top_visible_row = visible_rect.top // item_size.height bottom_visible_row = visible_rect.bottom // item_size.height left_visible_column = visible_rect.left // item_size.width right_visible_column = visible_rect.right // item_size.width for row in range(top_visible_row, bottom_visible_row + 1): for column in range(left_visible_column, right_visible_column + 1): if self.direction == Direction.Row: index = row * items_per_row + column else: index = row + column * items_per_column if 0 <= index < item_count: rect = Geometry.IntRect( origin=Geometry.IntPoint( y=row * item_size.height, x=column * item_size.width), size=Geometry.IntSize(width=item_size.width, height=item_size.height)) if rect.intersects_rect(visible_rect): is_selected = self.__selection.contains(index) if is_selected: with drawing_context.saver(): drawing_context.begin_path() drawing_context.rect( rect.left, rect.top, rect.width, rect.height) drawing_context.fill_style = "#3875D6" if self.focused else "#BBB" drawing_context.fill() self.__delegate.paint_item( drawing_context, items[index], rect, is_selected)
def wheel_changed(self, x: int, y: int, dx: int, dy: int, is_horizontal: bool) -> bool: dy = dy if not is_horizontal else 0 canvas_rect = self.canvas_rect if canvas_rect: new_canvas_origin = canvas_rect.origin + Geometry.IntPoint(x=0, y=dy) self.update_layout(new_canvas_origin, canvas_rect.size) self.update() return True
def __init__(self, stage_size_nm: float): self.__features = list() sample_size_m = Geometry.FloatSize(height=20 * stage_size_nm / 100, width=20 * stage_size_nm / 100) / 1E9 feature_percentage = 0.3 random_state = random.getstate() random.seed(1) energies = [[(68, 30), (855, 50), (872, 50)], [(29, 15), (1217, 50), (1248, 50)], [(1839, 5), (99, 50)]] # Ni, Ge, Si plasmons = [20, 16.2, 16.8] for i in range(100): position_m = Geometry.FloatPoint(y=(2 * random.random() - 1.0) * sample_size_m.height, x=(2 * random.random() - 1.0) * sample_size_m.width) size_m = feature_percentage * Geometry.FloatSize(height=random.random() * sample_size_m.height, width=random.random() * sample_size_m.width) self.__features.append( Feature(position_m, size_m, energies[i % len(energies)], plasmons[i % len(plasmons)], 4)) random.setstate(random_state)
def mouse_position_changed(self, x, y, modifiers): if super().mouse_position_changed(x, y, modifiers): return True if self.delegate.tool_mode == "pointer": self.cursor_shape = "arrow" self.__last_mouse = Geometry.IntPoint(x=x, y=y) self.__update_cursor_info() return True
def __create_thumbnail(self, draw_rect: Geometry.IntRect) -> DrawingContext.DrawingContext: drawing_context = DrawingContext.DrawingContext() if self.__display_item: thumbnail_data = self.calculate_thumbnail_data() if thumbnail_data is not None: draw_rect = Geometry.fit_to_size(draw_rect, thumbnail_data.shape) drawing_context.draw_image(thumbnail_data, draw_rect[0][1], draw_rect[0][0], draw_rect[1][1], draw_rect[1][0]) return drawing_context
def mouse_pressed(self, x, y, modifiers): if self.__delegate: mouse_index = y // self.__item_height max_index = self.__delegate.item_count if mouse_index >= 0 and mouse_index < max_index: self.__mouse_index = mouse_index self.__mouse_pressed = True handled = False if self.__delegate and hasattr(self.__delegate, "mouse_pressed_in_item") and self.__delegate.mouse_pressed_in_item: handled = self.__delegate.mouse_pressed_in_item(mouse_index, Geometry.IntPoint(y=y - mouse_index * self.__item_height, x=x), modifiers) if handled: self.__mouse_index = None # prevent selection handling if not handled and not modifiers.shift and not modifiers.control: self.__mouse_pressed_for_dragging = True self.__mouse_position = Geometry.IntPoint(y=y, x=x) return True return super().mouse_pressed(x, y, modifiers)
def create_dock_widget(self, widget: UserInterfaceModule.Widget, panel_id: str, title: str, positions: typing.Sequence[str], position: str) -> UserInterfaceModule.DockWidget: dock_widget = DockWidget(self, widget, panel_id, title, positions, position) dock_widget.size_changed(Geometry.IntSize(height=320, width=480)) return dock_widget
def drag_pressed(x, y, modifiers): on_drag = self.on_drag if callable(on_drag): mime_data = ui.create_mime_data() valid, thumbnail = thumbnail_source.populate_mime_data_for_drag( mime_data, Geometry.IntSize(width=80, height=80)) if valid: on_drag(mime_data, thumbnail, x, y)
def __init__(self, size=None): self.has_event_loop = False self.root_widget = None self.__menus = list() self.__size = size if size is not None else Geometry.IntSize( height=720, width=960) self.__dock_widgets = list() self.display_scaling = 1.0
def test_draw_data_with_color_table(self): dc = DrawingContext.DrawingContext() data = numpy.zeros((4, 4), numpy.float32) color_map_data = numpy.zeros((256, ), numpy.uint32) color_map_data[:] = 0xFF010203 dc.draw_data(data, 0, 0, 4, 4, 0, 1, color_map_data) dc.to_svg(Geometry.IntSize(4, 4), Geometry.IntRect.from_tlbr(0, 0, 4, 4))
def update_layout(self, canvas_origin, canvas_size, *, immediate=False): """Override from abstract canvas item. Adjust the canvas height based on the constraints. """ canvas_size = Geometry.IntSize.make(canvas_size) canvas_size = Geometry.IntSize(height=self.__calculate_layout_height(), width=canvas_size.width) super().update_layout(canvas_origin, canvas_size, immediate=immediate)
def grabbed_mouse_position_changed( self, dx: int, dy: int, modifiers: UserInterface.KeyboardModifiers) -> bool: if not self.__discard_first and callable( self.on_mouse_position_changed_by): self.on_mouse_position_changed_by(Geometry.IntPoint(x=dx, y=dy)) self.__discard_first = False return True
def context_menu_event(self, x, y, gx, gy): p = Geometry.IntPoint(y=y, x=x) value_path = self.__value_path_at_point(p) if value_path: if not self.__is_selected(value_path): self.__set_selection(value_path) return self.__context_menu_event(value_path, x, y, gx, gy) return self.__context_menu_event(None, x, y, gx, gy)
def _repaint_visible(self, drawing_context, visible_rect): if self.__delegate: canvas_bounds = self.canvas_bounds item_width = int(canvas_bounds.width) item_height = self.__item_height with drawing_context.saver(): items = self.__delegate.items max_index = len(items) top_visible_row = visible_rect.top // item_height bottom_visible_row = visible_rect.bottom // item_height for index in range(top_visible_row, bottom_visible_row + 1): if 0 <= index < max_index: rect = Geometry.IntRect( origin=Geometry.IntPoint(y=index * item_height, x=0), size=Geometry.IntSize(width=item_width, height=item_height)) if rect.intersects_rect(visible_rect): is_selected = self.__selection.contains(index) if is_selected: with drawing_context.saver(): drawing_context.begin_path() drawing_context.rect( rect.left, rect.top, rect.width, rect.height) drawing_context.fill_style = "#3875D6" if self.focused else "#DDD" drawing_context.fill() self.__delegate.paint_item(drawing_context, items[index], rect, is_selected) if index == self.__drop_index: with drawing_context.saver(): drop_border_width = 2.5 rect_in = rect.inset( drop_border_width / 2, drop_border_width / 2) drawing_context.begin_path() drawing_context.rect( rect_in.left, rect_in.top, rect_in.width, rect_in.height) drawing_context.line_width = drop_border_width drawing_context.stroke_style = "rgba(56, 117, 214, 0.8)" drawing_context.stroke()
def __init__(self, instrument_id: str): super().__init__() self.priority = 20 self.instrument_id = instrument_id self.property_changed_event = Event.Event() self.__camera_frame_event = threading.Event() # define the STEM geometry limits self.stage_size_nm = 1000 self.max_defocus = 5000 / 1E9 # define the samples self.__samples = [SampleSimulator.GrapheneSample(self), SampleSimulator.DefectGrapheneSample(self), SampleSimulator.DopedGrapheneSample(self), SampleSimulator.hBNSample(self), SampleSimulator.RectangleFlakeSample(self.stage_size_nm), SampleSimulator.AmorphousSample()] self.__sample_index = 0 self.__stage_position_m = Geometry.FloatPoint() self.__slit_in = False self.__energy_per_channel_eV = 0.5 self.__voltage = 100000 self.__beam_current = 200E-12 # 200 pA self.__blanked = False self.__ronchigram_shape = Geometry.IntSize(2048, 2048) self.__eels_shape = Geometry.IntSize(256, 1024) self.__scan_context = stem_controller.ScanContext() self.__probe_position = None self.__live_probe_position = None self.__sequence_progress = 0 self.__lock = threading.Lock() self.__controls = dict() built_in_controls = self.__create_built_in_controls() for control in built_in_controls: self.add_control(control) # We need to set the expressions after adding the controls to InstrumentDevice self.__set_expressions() self.__cameras = { "ronchigram": RonchigramCameraSimulator.RonchigramCameraSimulator(self, self.__ronchigram_shape, self.counts_per_electron, self.stage_size_nm), "eels": EELSCameraSimulator.EELSCameraSimulator(self, self.__eels_shape, self.counts_per_electron) }
def mouse_position_changed(self, x, y, modifiers): if self.__mouse_pressed: if not self.__mouse_dragging and Geometry.distance(self.__mouse_position, Geometry.IntPoint(y=y, x=x)) > 8: self.__mouse_dragging = True self.__drag_started(self.__mouse_item, x, y, modifiers) # once a drag starts, mouse release will not be called; call it here instead self.mouse_released(x, y, modifiers) return True return super().mouse_position_changed(x, y, modifiers)
def get_scan_rect_m(self, offset_m: Geometry.FloatPoint, fov_nm: Geometry.FloatSize, center_nm: Geometry.FloatPoint) -> Geometry.FloatRect: scan_size_m = Geometry.FloatSize(height=fov_nm.height, width=fov_nm.width) / 1E9 scan_rect_m = Geometry.FloatRect.from_center_and_size( Geometry.FloatPoint.make(center_nm) / 1E9, scan_size_m) scan_rect_m -= offset_m return scan_rect_m