SplineControl.pm 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. package Slic3r::GUI::Plater::SplineControl;
  2. use strict;
  3. use warnings;
  4. use utf8;
  5. use List::Util qw(min max first);
  6. use Slic3r::Geometry qw(X Y scale unscale);
  7. use Wx qw(:misc :pen :brush :sizer :font :cursor wxTAB_TRAVERSAL);
  8. use Wx::Event qw(EVT_MOUSE_EVENTS EVT_PAINT EVT_ERASE_BACKGROUND EVT_SIZE);
  9. use base 'Wx::Panel';
  10. sub new {
  11. my $class = shift;
  12. my ($parent, $size, $object) = @_;
  13. my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, $size, wxTAB_TRAVERSAL);
  14. $self->{object} = $object;
  15. $self->{is_valid} = 0;
  16. # This has only effect on MacOS. On Windows and Linux/GTK, the background is painted by $self->repaint().
  17. $self->SetBackgroundColour(Wx::wxWHITE);
  18. $self->{line_pen} = Wx::Pen->new(Wx::Colour->new(50,50,50), 1, wxSOLID);
  19. $self->{original_pen} = Wx::Pen->new(Wx::Colour->new(200,200,200), 1, wxSOLID);
  20. $self->{interactive_pen} = Wx::Pen->new(Wx::Colour->new(255,0,0), 1, wxSOLID);
  21. $self->{resulting_pen} = Wx::Pen->new(Wx::Colour->new(5,120,160), 1, wxSOLID);
  22. $self->{user_drawn_background} = $^O ne 'darwin';
  23. # scale plot data to actual canvas, documentation in set_size_parameters
  24. $self->{scaling_factor_x} = 1;
  25. $self->{scaling_factor_y} = 1;
  26. $self->{min_layer_height} = 0.1;
  27. $self->{max_layer_height} = 0.4;
  28. $self->{mousover_layer_height} = undef; # display layer height at mousover position
  29. $self->{object_height} = 1.0;
  30. # initialize values
  31. $self->update;
  32. EVT_PAINT($self, \&repaint);
  33. EVT_ERASE_BACKGROUND($self, sub {}) if $self->{user_drawn_background};
  34. EVT_MOUSE_EVENTS($self, \&mouse_event);
  35. EVT_SIZE($self, sub {
  36. $self->_update_canvas_size;
  37. $self->Refresh;
  38. });
  39. return $self;
  40. }
  41. sub repaint {
  42. my ($self, $event) = @_;
  43. my $dc = Wx::AutoBufferedPaintDC->new($self);
  44. my $size = $self->GetSize;
  45. my @size = ($size->GetWidth, $size->GetHeight);
  46. if ($self->{user_drawn_background}) {
  47. # On all systems the AutoBufferedPaintDC() achieves double buffering.
  48. # On MacOS the background is erased, on Windows the background is not erased
  49. # and on Linux/GTK the background is erased to gray color.
  50. # Fill DC with the background on Windows & Linux/GTK.
  51. my $brush_background = Wx::Brush->new(Wx::wxWHITE, wxSOLID);
  52. $dc->SetPen(wxWHITE_PEN);
  53. $dc->SetBrush($brush_background);
  54. my $rect = $self->GetUpdateRegion()->GetBox();
  55. $dc->DrawRectangle($rect->GetLeft(), $rect->GetTop(), $rect->GetWidth(), $rect->GetHeight());
  56. }
  57. # draw scale (min and max indicator at the bottom)
  58. $dc->SetTextForeground(Wx::Colour->new(0,0,0));
  59. $dc->SetFont(Wx::Font->new(10, wxDEFAULT, wxNORMAL, wxNORMAL));
  60. $dc->DrawLabel(sprintf('%.4g', $self->{min_layer_height}), Wx::Rect->new(0, $size[1]/2, $size[0], $size[1]/2), wxALIGN_LEFT | wxALIGN_BOTTOM);
  61. $dc->DrawLabel(sprintf('%.2g', $self->{max_layer_height}), Wx::Rect->new(0, $size[1]/2, $size[0], $size[1]/2), wxALIGN_RIGHT | wxALIGN_BOTTOM);
  62. if($self->{mousover_layer_height}){
  63. $dc->DrawLabel(sprintf('%4.2fmm', $self->{mousover_layer_height}), Wx::Rect->new(0, 0, $size[0], $size[1]), wxALIGN_RIGHT | wxALIGN_TOP);
  64. }
  65. if($self->{is_valid}) {
  66. # draw original spline as reference
  67. if($self->{original_height_spline}) {
  68. #draw spline
  69. $self->_draw_layer_height_spline($dc, $self->{original_height_spline}, $self->{original_pen});
  70. }
  71. # draw interactive (currently modified by the user) layers as lines and spline
  72. if($self->{interactive_height_spline}) {
  73. # draw layer lines
  74. my @interpolated_layers = @{$self->{interactive_height_spline}->getInterpolatedLayers};
  75. $self->_draw_layers_as_lines($dc, $self->{interactive_pen}, \@interpolated_layers);
  76. #draw spline
  77. $self->_draw_layer_height_spline($dc, $self->{interactive_height_spline}, $self->{interactive_pen});
  78. }
  79. # draw resulting layers as lines
  80. unless($self->{interactive_heights}) {
  81. $self->_draw_layers_as_lines($dc, $self->{resulting_pen}, $self->{interpolated_layers});
  82. }
  83. # Always draw current BSpline, gives a reference during a modification
  84. $self->_draw_layer_height_spline($dc, $self->{object}->layer_height_spline, $self->{line_pen});
  85. }
  86. $event->Skip;
  87. }
  88. # Set basic parameters for this control.
  89. # min/max_layer_height are required to define the x-range, object_height is used to scale the y-range.
  90. # Must be called if object selection changes.
  91. sub set_size_parameters {
  92. my ($self, $min_layer_height, $max_layer_height, $object_height) = @_;
  93. $self->{min_layer_height} = $min_layer_height;
  94. $self->{max_layer_height} = $max_layer_height;
  95. $self->{object_height} = $object_height;
  96. $self->_update_canvas_size;
  97. $self->Refresh;
  98. }
  99. # Layers have been modified externally, re-initialize this control with new values
  100. sub update {
  101. my $self = shift;
  102. if(($self->{object}->layer_height_spline->layersUpdated || !$self->{heights}) && $self->{object}->layer_height_spline->hasData) {
  103. $self->{original_height_spline} = $self->{object}->layer_height_spline->clone; # make a copy to display the unmodified original spline
  104. $self->{original_layers} = $self->{object}->layer_height_spline->getOriginalLayers;
  105. $self->{interpolated_layers} = $self->{object}->layer_height_spline->getInterpolatedLayers; # Initialize to current values
  106. # initialize height vector
  107. $self->{heights} = ();
  108. $self->{interactive_heights} = ();
  109. foreach my $z (@{$self->{original_layers}}) {
  110. push (@{$self->{heights}}, $self->{object}->layer_height_spline->getLayerHeightAt($z));
  111. }
  112. $self->{is_valid} = 1;
  113. }
  114. $self->Refresh;
  115. }
  116. # Callback to notify parent element if layers have changed and reslicing should be triggered
  117. sub on_layer_update {
  118. my ($self, $cb) = @_;
  119. $self->{on_layer_update} = $cb;
  120. }
  121. # Callback to tell parent element at which z-position the mouse currently hovers to update indicator in 3D-view
  122. sub on_z_indicator {
  123. my ($self, $cb) = @_;
  124. $self->{on_z_indicator} = $cb;
  125. }
  126. sub mouse_event {
  127. my ($self, $event) = @_;
  128. my $pos = $event->GetPosition;
  129. my @obj_pos = $self->pixel_to_point($pos);
  130. if ($event->ButtonDown) {
  131. if ($event->LeftDown) {
  132. # start dragging
  133. $self->{left_drag_start_pos} = $pos;
  134. $self->{interactive_height_spline} = $self->{object}->layer_height_spline->clone;
  135. }
  136. if ($event->RightDown) {
  137. # start dragging
  138. $self->{right_drag_start_pos} = $pos;
  139. $self->{interactive_height_spline} = $self->{object}->layer_height_spline->clone;
  140. }
  141. } elsif ($event->LeftUp) {
  142. if($self->{left_drag_start_pos}) {
  143. $self->_modification_done;
  144. }
  145. $self->{left_drag_start_pos} = undef;
  146. } elsif ($event->RightUp) {
  147. if($self->{right_drag_start_pos}) {
  148. $self->_modification_done;
  149. }
  150. $self->{right_drag_start_pos} = undef;
  151. } elsif ($event->Dragging) {
  152. if($self->{left_drag_start_pos}) {
  153. my @start_pos = $self->pixel_to_point($self->{left_drag_start_pos});
  154. my $range = abs($start_pos[1] - $obj_pos[1]);
  155. # compute updated interactive layer heights
  156. $self->_interactive_quadratic_curve($start_pos[1], $obj_pos[0], $range);
  157. unless($self->{interactive_height_spline}->updateLayerHeights($self->{interactive_heights})) {
  158. die "Unable to update interactive interpolated layers!\n";
  159. }
  160. $self->Refresh;
  161. } elsif($self->{right_drag_start_pos}) {
  162. my @start_pos = $self->pixel_to_point($self->{right_drag_start_pos});
  163. my $range = $obj_pos[1] - $start_pos[1];
  164. # compute updated interactive layer heights
  165. $self->_interactive_linear_curve($start_pos[1], $obj_pos[0], $range);
  166. unless($self->{interactive_height_spline}->updateLayerHeights($self->{interactive_heights})) {
  167. die "Unable to update interactive interpolated layers!\n";
  168. }
  169. $self->Refresh;
  170. }
  171. } elsif ($event->Moving) {
  172. if($self->{on_z_indicator}) {
  173. $self->{on_z_indicator}->($obj_pos[1]);
  174. $self->{mousover_layer_height} = $self->{object}->layer_height_spline->getLayerHeightAt($obj_pos[1]);
  175. $self->Refresh;
  176. $self->Update;
  177. }
  178. } elsif ($event->Leaving) {
  179. if($self->{on_z_indicator} && !$self->{left_drag_start_pos}) {
  180. $self->{on_z_indicator}->(undef);
  181. }
  182. $self->{mousover_layer_height} = undef;
  183. $self->Refresh;
  184. $self->Update;
  185. }
  186. }
  187. # Push modified heights to the spline object and update after user modification
  188. sub _modification_done {
  189. my $self = shift;
  190. if($self->{interactive_heights}) {
  191. $self->{heights} = $self->{interactive_heights};
  192. $self->{interactive_heights} = ();
  193. # update spline database
  194. unless($self->{object}->layer_height_spline->updateLayerHeights($self->{heights})) {
  195. die "Unable to update interpolated layers!\n";
  196. }
  197. $self->{interpolated_layers} = $self->{object}->layer_height_spline->getInterpolatedLayers;
  198. }
  199. $self->Refresh;
  200. $self->{on_layer_update}->(@{$self->{interpolated_layers}});
  201. $self->{interactive_height_spline} = undef;
  202. }
  203. # Internal function to cache scaling factors
  204. sub _update_canvas_size {
  205. my $self = shift;
  206. # when the canvas is not rendered yet, its GetSize() method returns 0,0
  207. my $canvas_size = $self->GetSize;
  208. my ($canvas_w, $canvas_h) = ($canvas_size->GetWidth, $canvas_size->GetHeight);
  209. return if $canvas_w == 0;
  210. my $padding = $self->{canvas_padding} = 10; # border size in pixels
  211. my @size = ($canvas_w - 2*$padding, $canvas_h - 2*$padding);
  212. $self->{canvas_size} = [@size];
  213. $self->{scaling_factor_x} = $size[0]/($self->{max_layer_height} - $self->{min_layer_height});
  214. $self->{scaling_factor_y} = $size[1]/$self->{object_height};
  215. }
  216. # calculate new set of layers with quadaratic modifier for interactive display
  217. sub _interactive_quadratic_curve {
  218. my ($self, $mod_z, $target_layer_height, $range) = @_;
  219. $self->{interactive_heights} = (); # reset interactive curve
  220. # iterate over original points provided by spline
  221. my $last_z = 0;
  222. foreach my $i (0..@{$self->{heights}}-1 ) {
  223. my $z = $self->{original_layers}[$i];
  224. my $layer_h = $self->{heights}[$i];
  225. my $quadratic_factor = $self->_quadratic_factor($mod_z, $range, $z);
  226. my $diff = $target_layer_height - $layer_h;
  227. $layer_h += $diff * $quadratic_factor;
  228. push (@{$self->{interactive_heights}}, $layer_h);
  229. }
  230. }
  231. # calculate new set of layers with linear modifier for interactive display
  232. sub _interactive_linear_curve {
  233. my ($self, $mod_z, $target_layer_height, $range) = @_;
  234. $self->{interactive_heights} = (); # reset interactive curve
  235. my $from;
  236. my $to;
  237. if($range >= 0) {
  238. $from = $mod_z;
  239. $to = $mod_z + $range;
  240. }else{
  241. $from = $mod_z + $range;
  242. $to = $mod_z;
  243. }
  244. # iterate over original points provided by spline
  245. foreach my $i (0..@{$self->{heights}}-1 ) {
  246. if(($self->{original_layers}[$i]) >= $from && ($self->{original_layers}[$i]< $to)) {
  247. push (@{$self->{interactive_heights}}, $target_layer_height);
  248. }else{
  249. push (@{$self->{interactive_heights}}, $self->{heights}[$i]);
  250. }
  251. }
  252. }
  253. sub _quadratic_factor {
  254. my ($self, $fixpoint, $range, $value) = @_;
  255. # avoid division by zero
  256. $range = 0.00001 if $range <= 0;
  257. my $dist = abs($fixpoint - $value);
  258. my $x = $dist/$range; # normalize
  259. my $result = 1-($x*$x);
  260. return max(0, $result);
  261. }
  262. # Draw a set of layers as lines
  263. sub _draw_layers_as_lines {
  264. my ($self, $dc, $pen, $layers) = @_;
  265. $dc->SetPen($pen);
  266. my $last_z = 0.0;
  267. foreach my $z (@$layers) {
  268. my $layer_h = $z - $last_z;
  269. my $pl = $self->point_to_pixel(0, $z);
  270. my $pr = $self->point_to_pixel($layer_h, $z);
  271. $dc->DrawLine($pl->x, $pl->y, $pr->x, $pr->y);
  272. $last_z = $z;
  273. }
  274. }
  275. # Draw the resulting spline from a LayerHeightSpline object over the full canvas height
  276. sub _draw_layer_height_spline {
  277. my ($self, $dc, $layer_height_spline, $pen) = @_;
  278. my @size = @{$self->{canvas_size}};
  279. $dc->SetPen($pen);
  280. my @points = ();
  281. foreach my $pixel (0..$size[1]) {
  282. my @z = $self->pixel_to_point(Wx::Point->new(0, $pixel));
  283. my $h = $layer_height_spline->getLayerHeightAt($z[1]);
  284. my $p = $self->point_to_pixel($h, $z[1]);
  285. push (@points, $p);
  286. }
  287. $dc->DrawLines(\@points);
  288. }
  289. # Takes a 2-tupel [layer_height (x), height(y)] and converts it
  290. # into a Wx::Point in scaled canvas coordinates
  291. sub point_to_pixel {
  292. my ($self, @point) = @_;
  293. my @size = @{$self->{canvas_size}};
  294. my $x = ($point[0] - $self->{min_layer_height})*$self->{scaling_factor_x} + $self->{canvas_padding};
  295. my $y = $size[1] - $point[1]*$self->{scaling_factor_y} + $self->{canvas_padding}; # invert y-axis
  296. return Wx::Point->new(min(max($x, $self->{canvas_padding}), $size[0]+$self->{canvas_padding}), min(max($y, $self->{canvas_padding}), $size[1]+$self->{canvas_padding})); # limit to canvas size
  297. }
  298. # Takes a Wx::Point in scaled canvas coordinates and converts it
  299. # into a 2-tupel [layer_height (x), height(y)]
  300. sub pixel_to_point {
  301. my ($self, $point) = @_;
  302. my @size = @{$self->{canvas_size}};
  303. my $x = ($point->x-$self->{canvas_padding})/$self->{scaling_factor_x} + $self->{min_layer_height};
  304. my $y = ($size[1] - $point->y)/$self->{scaling_factor_y}; # invert y-axis
  305. return (min(max($x, $self->{min_layer_height}), $self->{max_layer_height}), min(max($y, 0), $self->{object_height})); # limit to object size and layer constraints
  306. }
  307. 1;