class DefaultOperationUI(OperationUI): """An extension of the base OperationUI that adds camera, static selection and some other controls.""" def __init__( self, parent: wx.Window, canvas: "EditCanvas", world: "BaseLevel", options_path: str, ): super().__init__(parent, canvas, world, options_path) self._selection = StaticSelectionBehaviour(self.canvas) self._camera_behaviour = CameraBehaviour(self.canvas) self._pointer = PointerBehaviour(self.canvas) self._show_pointer = False def enable(self): self._selection.update_selection() self.canvas.camera.projection_mode = Projection.PERSPECTIVE def bind_events(self): self._selection.bind_events() self.canvas.Bind(EVT_DRAW, self._on_draw) self._camera_behaviour.bind_events() self._pointer.bind_events() self.canvas.Bind(EVT_INPUT_PRESS, self._on_input_press) def _on_draw(self, evt): self.canvas.renderer.start_draw() if self.canvas.camera.projection_mode == Projection.PERSPECTIVE: self.canvas.renderer.draw_sky_box() glClear(GL_DEPTH_BUFFER_BIT) self.canvas.renderer.draw_level() self._selection.draw() if self._show_pointer: self._pointer.draw() self.canvas.renderer.end_draw() def _on_input_press(self, evt: InputPressEvent): if evt.action_id == ACT_BOX_CLICK: self._on_box_click() evt.Skip() def _on_box_click(self): pass
class PasteTool(wx.BoxSizer, DefaultBaseToolUI): def __init__(self, canvas: "EditCanvas"): wx.BoxSizer.__init__(self, wx.HORIZONTAL) DefaultBaseToolUI.__init__(self, canvas) self._selection = StaticSelectionBehaviour(self.canvas) self._cursor = PointerBehaviour(self.canvas) self._moving = False self._button_panel = wx.Panel(canvas) self._button_sizer = wx.BoxSizer(wx.VERTICAL) self._button_panel.SetSizer(self._button_sizer) self.Add(self._button_panel, 0, wx.ALIGN_CENTER_VERTICAL) self._paste_panel = SelectTransformUI(self._button_panel) self._paste_panel.Bind(EVT_TRANSFORM_CHANGE, self._on_transform_change) self._button_sizer.Add(self._paste_panel, 0, wx.EXPAND) confirm_button = wx.Button(self._button_panel, label="Confirm") self._button_sizer.Add(confirm_button, 0, wx.ALL | wx.EXPAND, 5) confirm_button.Bind(wx.EVT_BUTTON, self._paste_confirm) self._button_panel.Disable() self.Layout() @property def name(self) -> str: return "Paste" def bind_events(self): super().bind_events() self._selection.bind_events() self.canvas.Bind(EVT_PASTE, self._paste) self._cursor.bind_events() self.canvas.Bind(EVT_POINT_CHANGE, self._on_pointer_change) self.canvas.Bind(EVT_INPUT_PRESS, self._on_input_press) def enable(self): super().enable() self._selection.update_selection() self._moving = False def disable(self): super().disable() self._button_panel.Disable() self.canvas.renderer.fake_levels.clear() def _on_pointer_change(self, evt: PointChangeEvent): if self._moving: self.canvas.renderer.fake_levels.active_transform = ( evt.point, self._paste_panel.scale, self._paste_panel.rotation_radians, ) self._paste_panel.location = evt.point evt.Skip() def _on_transform_change(self, evt: TransformChangeEvent): self.canvas.renderer.fake_levels.active_transform = ( evt.location, evt.scale, evt.rotation_radians, ) evt.Skip() def _on_input_press(self, evt: InputPressEvent): if evt.action_id == ACT_BOX_CLICK: self._moving = not self._moving if self._moving: self.canvas.renderer.fake_levels.active_transform = ( self._paste_panel.location, self._paste_panel.scale, self._paste_panel.rotation_radians, ) evt.Skip() def _paste(self, evt): self._button_panel.Enable() structure = evt.structure dimension = evt.dimension self.canvas.renderer.fake_levels.clear() self.canvas.renderer.fake_levels.append(structure, dimension, (0, 0, 0), (1, 1, 1), (0, 0, 0)) self._moving = True def _paste_confirm(self, evt): fake_levels = self.canvas.renderer.fake_levels level_index: int = fake_levels.active_level_index if level_index is not None: render_level: RenderLevel = fake_levels.render_levels[level_index] self.canvas.run_operation(lambda: paste_iter( self.canvas.world, self.canvas.dimension, render_level.level, render_level.dimension, self._paste_panel.location, self._paste_panel.scale, self._paste_panel.rotation, self._paste_panel.copy_air, self._paste_panel.copy_water, self._paste_panel.copy_lava, )) def _on_draw(self, evt): self.canvas.renderer.start_draw() if self.canvas.camera.projection_mode == Projection.PERSPECTIVE: self.canvas.renderer.draw_sky_box() glClear(GL_DEPTH_BUFFER_BIT) self.canvas.renderer.draw_level() self.canvas.renderer.draw_fake_levels() self._selection.draw() self.canvas.renderer.end_draw()
class PasteTool(wx.BoxSizer, CameraToolUI): def __init__(self, canvas: "EditCanvas"): wx.BoxSizer.__init__(self, wx.HORIZONTAL) CameraToolUI.__init__(self, canvas) self._selection = StaticSelectionBehaviour(self.canvas) self._button_panel = wx.Panel(canvas) self._button_sizer = wx.BoxSizer(wx.VERTICAL) self._button_panel.SetSizer(self._button_sizer) paste_button = wx.Button(self._button_panel, label="Paste") self._button_sizer.Add(paste_button, 0, wx.ALL | wx.EXPAND, 5) paste_button.Bind(wx.EVT_BUTTON, lambda evt: self.canvas.paste_from_cache()) self.Add(self._button_panel, 0, wx.ALIGN_CENTER_VERTICAL) self._paste_panel: Optional[SelectTransformUI] = None self.Layout() @property def name(self) -> str: return "Paste" def bind_events(self): super().bind_events() self._selection.bind_events() self.canvas.Bind(EVT_PASTE, self._paste) def enable(self): super().enable() self._selection.update_selection() def disable(self): super().disable() self.canvas.renderer.fake_levels.unload() def _remove_paste(self): if self._paste_panel is not None: self._paste_panel.Destroy() self._paste_panel = None def _paste(self, evt): structure = evt.structure dimension = evt.dimension self._remove_paste() self._paste_panel = SelectTransformUI(self._button_panel, self.canvas, structure, dimension, self._paste_confirm) self._button_sizer.Add(self._paste_panel, 0, wx.EXPAND) self.Layout() def _paste_confirm(self): self.canvas.run_operation(lambda: paste_iter( self.canvas.world, self.canvas.dimension, self._paste_panel.structure, self._paste_panel.dimension, self._paste_panel.location, (1, 1, 1), self._paste_panel.rotation, self._paste_panel.copy_air, self._paste_panel.copy_water, self._paste_panel.copy_lava, )) def _on_draw(self, evt): self.canvas.renderer.start_draw() if self.canvas.camera.projection_mode == Projection.PERSPECTIVE: self.canvas.renderer.draw_sky_box() glClear(GL_DEPTH_BUFFER_BIT) self.canvas.renderer.draw_level() self.canvas.renderer.draw_fake_levels() self._selection.draw() self.canvas.renderer.end_draw()
class PasteTool(wx.BoxSizer, DefaultBaseToolUI): def __init__(self, canvas: "EditCanvas"): wx.BoxSizer.__init__(self, wx.HORIZONTAL) DefaultBaseToolUI.__init__(self, canvas) self._selection = StaticSelectionBehaviour(self.canvas) self._cursor = PointerBehaviour(self.canvas) self._moving = False self._is_enabled = False self._paste_panel = wx.Panel(canvas) self._paste_sizer = wx.BoxSizer(wx.VERTICAL) self._paste_panel.SetSizer(self._paste_sizer) self.Add(self._paste_panel, 0, wx.ALIGN_CENTER_VERTICAL) def add_line(): """add a line to the UI""" line = wx.StaticLine(self._paste_panel) self._paste_sizer.Add(line, 0, wx.BOTTOM | wx.EXPAND, 5) def add_tick_box(name: str, state: bool = True): tick = wx.CheckBox(self._paste_panel, label=name) tick.SetValue(state) self._paste_sizer.Add( tick, flag=BottomLeftRight, border=5, ) return tick def add_label(name: str): label = wx.StaticText(self._paste_panel, label=name) label.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, True, )) self._paste_sizer.Add(label, 0, BottomLeftRightCentre, 5) self._paste_sizer.AddSpacer(5) add_label(lang.get("program_3d_edit.paste_tool.location_label")) self._location = TupleIntInput( self._paste_panel, lang.get("program_3d_edit.paste_tool.location_x_label"), lang.get("program_3d_edit.paste_tool.location_y_label"), lang.get("program_3d_edit.paste_tool.location_z_label"), ) self._location.x.SetToolTip( lang.get("program_3d_edit.paste_tool.location_x_tooltip")) self._location.y.SetToolTip( lang.get("program_3d_edit.paste_tool.location_y_tooltip")) self._location.z.SetToolTip( lang.get("program_3d_edit.paste_tool.location_z_tooltip")) self._paste_sizer.Add( self._location, flag=BottomLeftRightExpand, border=5, ) self._move_button = MoveButton( self._paste_panel, self.canvas.camera, self.canvas.key_binds, lang.get("program_3d_edit.paste_tool.move_selection_label"), lang.get("program_3d_edit.paste_tool.move_selection_tooltip"), self, ) self._paste_sizer.Add( self._move_button, flag=BottomLeftRightExpand, border=5, ) add_line() add_label(lang.get("program_3d_edit.paste_tool.rotation_label")) self._free_rotation = wx.CheckBox( self._paste_panel, label=lang.get("program_3d_edit.paste_tool.free_rotation_label"), ) self._free_rotation.SetToolTip( lang.get("program_3d_edit.paste_tool.free_rotation_tooltip")) self._paste_sizer.Add( self._free_rotation, flag=BottomLeftRight, border=5, ) self._rotation = RotationTupleInput( self._paste_panel, lang.get("program_3d_edit.paste_tool.rotation_x_label"), lang.get("program_3d_edit.paste_tool.rotation_y_label"), lang.get("program_3d_edit.paste_tool.rotation_z_label"), ) self._rotation.x.SetToolTip( lang.get("program_3d_edit.paste_tool.rotation_x_tooltip")) self._rotation.y.SetToolTip( lang.get("program_3d_edit.paste_tool.rotation_y_tooltip")) self._rotation.z.SetToolTip( lang.get("program_3d_edit.paste_tool.rotation_z_tooltip")) self._paste_sizer.Add( self._rotation, flag=BottomLeftRightExpand, border=5, ) self._free_rotation.Bind(wx.EVT_CHECKBOX, self._on_free_rotation_change) rotate_sizer = wx.BoxSizer(wx.HORIZONTAL) self._paste_sizer.Add( rotate_sizer, flag=BottomLeftRightCentre, border=5, ) self._rotate_left_button = wx.BitmapButton( self._paste_panel, bitmap=image.icon.tablericons.rotate_2.bitmap(22, 22)) self._rotate_left_button.SetToolTip( lang.get( "program_3d_edit.paste_tool.rotate_anti_clockwise_tooltip")) self._rotate_left_button.Bind(wx.EVT_BUTTON, self._on_rotate_left) rotate_sizer.Add(self._rotate_left_button) self._rotate_right_button = wx.BitmapButton( self._paste_panel, bitmap=image.icon.tablericons.rotate_clockwise_2.bitmap(22, 22), ) self._rotate_right_button.SetToolTip( lang.get("program_3d_edit.paste_tool.rotate_clockwise_tooltip")) self._rotate_right_button.Bind(wx.EVT_BUTTON, self._on_rotate_right) rotate_sizer.Add(self._rotate_right_button) add_line() add_label(lang.get("program_3d_edit.paste_tool.scale_label")) self._scale = TupleFloatInput( self._paste_panel, lang.get("program_3d_edit.paste_tool.scale_x_label"), lang.get("program_3d_edit.paste_tool.scale_y_label"), lang.get("program_3d_edit.paste_tool.scale_z_label"), start_value=1, ) self._scale.x.SetToolTip( lang.get("program_3d_edit.paste_tool.scale_x_tooltip")) self._scale.y.SetToolTip( lang.get("program_3d_edit.paste_tool.scale_y_tooltip")) self._scale.z.SetToolTip( lang.get("program_3d_edit.paste_tool.scale_z_tooltip")) self._scale.x.SetDigits(2) self._scale.y.SetDigits(2) self._scale.z.SetDigits(2) self._paste_sizer.Add( self._scale, flag=BottomLeftRightExpand, border=5, ) self._paste_panel.Bind(wx.EVT_SPINCTRL, self._on_transform_change) self._paste_panel.Bind(wx.EVT_SPINCTRLDOUBLE, self._on_transform_change) mirror_sizer = wx.BoxSizer(wx.HORIZONTAL) self._paste_sizer.Add( mirror_sizer, flag=BottomLeftRightCentre, border=5, ) # the tablericons file names are the wrong way around self._mirror_horizontal_button = wx.BitmapButton( self._paste_panel, bitmap=image.icon.tablericons.flip_vertical.bitmap(22, 22), ) self._mirror_horizontal_button.SetToolTip( lang.get("program_3d_edit.paste_tool.mirror_horizontal_tooltip")) self._mirror_horizontal_button.Bind(wx.EVT_BUTTON, self._on_mirror_horizontal) mirror_sizer.Add(self._mirror_horizontal_button) self._mirror_vertical_button = wx.BitmapButton( self._paste_panel, bitmap=image.icon.tablericons.flip_horizontal.bitmap(22, 22), ) self._mirror_vertical_button.SetToolTip( lang.get("program_3d_edit.paste_tool.mirror_vertical_tooltip")) self._mirror_vertical_button.Bind(wx.EVT_BUTTON, self._on_mirror_vertical) mirror_sizer.Add(self._mirror_vertical_button) add_line() self._copy_air = add_tick_box( lang.get("program_3d_edit.paste_tool.copy_air_label")) self._copy_air.SetToolTip( lang.get("program_3d_edit.paste_tool.copy_air_tooltip")) self._copy_water = add_tick_box( lang.get("program_3d_edit.paste_tool.copy_water_label")) self._copy_water.SetToolTip( lang.get("program_3d_edit.paste_tool.copy_water_tooltip")) self._copy_lava = add_tick_box( lang.get("program_3d_edit.paste_tool.copy_lava_label")) self._copy_lava.SetToolTip( lang.get("program_3d_edit.paste_tool.copy_lava_tooltip")) add_line() confirm_button = wx.Button(self._paste_panel, label="Confirm") self._paste_sizer.Add(confirm_button, 0, BottomLeftRightExpand, 5) confirm_button.Bind(wx.EVT_BUTTON, self._paste_confirm) self._paste_panel.Disable() self.Layout() @property def name(self) -> str: return "Paste" def bind_events(self): super().bind_events() self._selection.bind_events() self.canvas.Bind(EVT_PASTE, self._paste) self._cursor.bind_events() self.canvas.Bind(EVT_POINT_CHANGE, self._on_pointer_change) self.canvas.Bind(EVT_INPUT_PRESS, self._on_input_press) def enable(self): super().enable() self._move_button.enable() self._selection.update_selection() self._moving = False def disable(self): super().disable() self._move_button.disable() self._paste_panel.Disable() self._is_enabled = False self.canvas.renderer.fake_levels.clear() @property def location(self) -> PointCoordinates: """The location as specified in the UI.""" return self._location.value @location.setter def location(self, location: PointCoordinates): """Set the location value. Will update the UI and the renderer.""" self._location.value = location self._update_transform() def _on_free_rotation_change(self, evt): if self._free_rotation.GetValue(): self._rotation.increment = 1 else: self._rotation.increment = 90 def _on_rotate_left(self, evt): self._rotate(-90) def _on_rotate_right(self, evt): self._rotate(90) def _rotate(self, angle: int): """Rotate the floating selection by the angle based on the camera rotation.""" angle = math.radians(angle) ry, rx = self.canvas.camera.rotation if rx < -45: rotation_change = rotation_matrix_xyz(0, angle, 0) elif -45 <= rx < 45: if -135 <= ry < -45: # east rotation_change = rotation_matrix_xyz(angle, 0, 0) elif -45 <= ry < 45: # south rotation_change = rotation_matrix_xyz(0, 0, angle) elif 45 <= ry < 135: # west rotation_change = rotation_matrix_xyz(-angle, 0, 0) else: # north rotation_change = rotation_matrix_xyz(0, 0, -angle) else: rotation_change = rotation_matrix_xyz(0, -angle, 0) self._rotation.value = numpy.rad2deg( decompose_transformation_matrix( numpy.matmul( rotation_change, rotation_matrix_xyz(*self._rotation_radians())))[1]) self._update_transform() def _rotation_radians(self) -> Tuple[float, float, float]: return tuple(math.radians(v) for v in self._rotation.value) def _on_mirror_vertical(self, evt): ry, rx = self.canvas.camera.rotation if -45 <= rx < 45: # looking north, east, south or west vertical mirror is always in y self._mirror(1) elif -135 <= ry < -45 or 45 <= ry < 135: # looking down or up facing east or west self._mirror(0) else: # looking down or up facing north or south self._mirror(2) def _on_mirror_horizontal(self, evt): ry, rx = self.canvas.camera.rotation if -135 <= ry < -45 or 45 <= ry < 135: # facing east or west self._mirror(2) else: # facing north or south self._mirror(0) def _mirror(self, axis: int): """Mirror the selection in the given axis. :param axis: The axis to scale in 0=x, 1=y, 2=z :return: """ scale = [(-1, 1, 1), (1, -1, 1), (1, 1, -1)][axis] self._scale.value, rotation, _ = decompose_transformation_matrix( numpy.matmul( scale_matrix(*scale), transform_matrix(self._scale.value, self._rotation_radians(), (0, 0, 0)), )) self._rotation.value = numpy.rad2deg(rotation) self._update_transform() def _on_pointer_change(self, evt: PointChangeEvent): if self._is_enabled and self._moving: self.canvas.renderer.fake_levels.active_transform = ( evt.point, self._scale.value, self._rotation_radians(), ) self._location.value = evt.point evt.Skip() def _on_transform_change(self, evt): self._update_transform() evt.Skip() def _update_transform(self): """Update the renderer with the new values.""" self.canvas.renderer.fake_levels.active_transform = ( self._location.value, self._scale.value, self._rotation_radians(), ) def _on_input_press(self, evt: InputPressEvent): if evt.action_id == ACT_BOX_CLICK: if self._is_enabled: self._moving = not self._moving if self._moving: self.canvas.renderer.fake_levels.active_transform = ( self._location.value, self._scale.value, self._rotation_radians(), ) evt.Skip() def _paste(self, evt): self._paste_panel.Enable() self._is_enabled = True structure = evt.structure dimension = evt.dimension self.canvas.renderer.fake_levels.clear() self.canvas.renderer.fake_levels.append(structure, dimension, (0, 0, 0), (1, 1, 1), (0, 0, 0)) self._moving = True def _paste_operation(self): if all(self._scale.value): fake_levels = self.canvas.renderer.fake_levels level_index: int = fake_levels.active_level_index if level_index is not None: render_level: RenderLevel = fake_levels.render_levels[ level_index] yield from paste_iter( self.canvas.world, self.canvas.dimension, render_level.level, render_level.dimension, self._location.value, self._scale.value, self._rotation.value, self._copy_air.GetValue(), self._copy_water.GetValue(), self._copy_lava.GetValue(), ) else: raise OperationSuccessful( lang.get("program_3d_edit.paste_tool.zero_scale_message")) def _paste_confirm(self, evt): self.canvas.run_operation(self._paste_operation) def _on_draw(self, evt): self.canvas.renderer.start_draw() if self.canvas.camera.projection_mode == Projection.PERSPECTIVE: self.canvas.renderer.draw_sky_box() glClear(GL_DEPTH_BUFFER_BIT) self.canvas.renderer.draw_level() self.canvas.renderer.draw_fake_levels() self._selection.draw() self.canvas.renderer.end_draw()
class BaseSelectOperationUI(wx.BoxSizer, CameraToolUI): OperationGroupName = None def __init__(self, canvas: "EditCanvas"): wx.BoxSizer.__init__(self, wx.VERTICAL) CameraToolUI.__init__(self, canvas) self._selection = StaticSelectionBehaviour(self.canvas) self._active_operation: Optional[OperationUIType] = None horizontal_sizer = wx.BoxSizer(wx.HORIZONTAL) self._operation_choice = SimpleChoiceAny(self.canvas) self._reload_operation = wx.BitmapButton( self.canvas, bitmap=REFRESH_ICON.bitmap(16, 16) ) self._reload_operation.SetToolTip("Reload Operations") horizontal_sizer.Add(self._operation_choice) horizontal_sizer.Add(self._reload_operation) self.Add(horizontal_sizer) assert isinstance( self.OperationGroupName, str ), "OperationGroupName has not been set or is not a string." self._operations = UIOperationManager(self.OperationGroupName) self._operation_choice.SetItems( {op.identifier: op.name for op in self._operations.operations} ) self._operation_choice.Bind(wx.EVT_CHOICE, self._on_operation_change) self._reload_operation.Bind(wx.EVT_BUTTON, self._on_reload_operations) self._operation_sizer = wx.BoxSizer(wx.VERTICAL) self.Add(self._operation_sizer, 1, wx.EXPAND) @property def name(self) -> str: raise NotImplementedError @property def operation(self) -> str: return self._operation_choice.GetCurrentObject() def _unload_active_operation(self): """Unload and destroy the UI for the active operation.""" if self._active_operation is not None: self._active_operation.unload() if isinstance(self._active_operation, wx.Window): self._active_operation.Destroy() elif isinstance(self._active_operation, wx.Sizer): self._operation_sizer.GetItem(self._active_operation).DeleteWindows() self._active_operation = None def _on_operation_change(self, evt): """Run when the operation selection changes.""" self._setup_operation() evt.Skip() def _setup_operation(self): """Create the UI for the new operation.""" operation_path = self._operation_choice.GetCurrentObject() if operation_path: operation = self._operations[operation_path] self._unload_active_operation() self._active_operation = operation( self.canvas, self.canvas, self.canvas.world ) self._operation_sizer.Add( self._active_operation, *self._active_operation.wx_add_options ) self.Layout() def bind_events(self): super().bind_events() self._selection.bind_events() def enable(self): super().enable() self._selection.update_selection() self._setup_operation() def disable(self): super().disable() self._unload_active_operation() def _on_reload_operations(self, evt): """Run when the button is pressed to reload the operations.""" self.reload_operations() def reload_operations(self): """Reload all operations and repopulate the UI.""" # store the id of the old operation operation_id = self.operation # reload the operations self._operations.reload() # repopulate the selection self._operation_choice.SetItems( {op.identifier: op.name for op in self._operations.operations} ) if operation_id: operation_loader = self._operations[operation_id] identifiers = self._operation_choice.values if identifiers: if operation_id in identifiers: self._operation_choice.SetSelection(identifiers.index(operation_id)) else: log.info(f"Operation {operation_id} was not found.") self._operation_choice.SetSelection(0) else: log.error("No operations found. Something has gone wrong.") self._setup_operation() def _on_draw(self, evt): self.canvas.renderer.start_draw() if self.canvas.camera.projection_mode == Projection.PERSPECTIVE: self.canvas.renderer.draw_sky_box() glClear(GL_DEPTH_BUFFER_BIT) self.canvas.renderer.draw_level() self._selection.draw() self.canvas.renderer.end_draw()