User interface of the variable layer thickness. Certainly not finished yet,
but sufficient for evaluation of the prints.
This commit is contained in:
parent
2d030f3a3c
commit
46b44fc141
5 changed files with 413 additions and 133 deletions
|
@ -53,10 +53,15 @@ __PACKAGE__->mk_accessors( qw(_quat _dirty init
|
||||||
origin
|
origin
|
||||||
_mouse_pos
|
_mouse_pos
|
||||||
_hover_volume_idx
|
_hover_volume_idx
|
||||||
|
|
||||||
_drag_volume_idx
|
_drag_volume_idx
|
||||||
_drag_start_pos
|
_drag_start_pos
|
||||||
_drag_start_xy
|
_drag_start_xy
|
||||||
_dragged
|
_dragged
|
||||||
|
|
||||||
|
layer_editing_enabled
|
||||||
|
_layer_height_edited
|
||||||
|
|
||||||
_camera_type
|
_camera_type
|
||||||
_camera_target
|
_camera_target
|
||||||
_camera_distance
|
_camera_distance
|
||||||
|
@ -74,7 +79,7 @@ use constant SELECTED_COLOR => [0,1,0,1];
|
||||||
use constant HOVER_COLOR => [0.4,0.9,0,1];
|
use constant HOVER_COLOR => [0.4,0.9,0,1];
|
||||||
|
|
||||||
# phi / theta angles to orient the camera.
|
# phi / theta angles to orient the camera.
|
||||||
use constant VIEW_ISO => [45.0,45.0];
|
use constant VIEW_DEFAULT => [45.0,45.0];
|
||||||
use constant VIEW_LEFT => [90.0,90.0];
|
use constant VIEW_LEFT => [90.0,90.0];
|
||||||
use constant VIEW_RIGHT => [-90.0,90.0];
|
use constant VIEW_RIGHT => [-90.0,90.0];
|
||||||
use constant VIEW_TOP => [0.0,0.0];
|
use constant VIEW_TOP => [0.0,0.0];
|
||||||
|
@ -82,7 +87,11 @@ use constant VIEW_BOTTOM => [0.0,180.0];
|
||||||
use constant VIEW_FRONT => [0.0,90.0];
|
use constant VIEW_FRONT => [0.0,90.0];
|
||||||
use constant VIEW_REAR => [180.0,90.0];
|
use constant VIEW_REAR => [180.0,90.0];
|
||||||
|
|
||||||
use constant GIMBAL_LOCK_THETA_MAX => 170;
|
use constant MANIPULATION_IDLE => 0;
|
||||||
|
use constant MANIPULATION_DRAGGING => 1;
|
||||||
|
use constant MANIPULATION_LAYER_HEIGHT => 2;
|
||||||
|
|
||||||
|
use constant GIMBALL_LOCK_THETA_MAX => 170;
|
||||||
|
|
||||||
# make OpenGL::Array thread-safe
|
# make OpenGL::Array thread-safe
|
||||||
{
|
{
|
||||||
|
@ -130,6 +139,11 @@ sub new {
|
||||||
$self->_camera_target(Slic3r::Pointf3->new(0,0,0));
|
$self->_camera_target(Slic3r::Pointf3->new(0,0,0));
|
||||||
$self->_camera_distance(0.);
|
$self->_camera_distance(0.);
|
||||||
|
|
||||||
|
# Size of a layer height texture, used by a shader to color map the object print layers.
|
||||||
|
$self->{layer_preview_z_texture_width} = 512;
|
||||||
|
$self->{layer_preview_z_texture_height} = 512;
|
||||||
|
$self->{layer_height_edit_band_width} = 2.;
|
||||||
|
|
||||||
$self->reset_objects;
|
$self->reset_objects;
|
||||||
|
|
||||||
EVT_PAINT($self, sub {
|
EVT_PAINT($self, sub {
|
||||||
|
@ -177,6 +191,16 @@ sub new {
|
||||||
return $self;
|
return $self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub _first_selected_object_id {
|
||||||
|
my ($self) = @_;
|
||||||
|
for my $i (0..$#{$self->volumes}) {
|
||||||
|
if ($self->volumes->[$i]->selected) {
|
||||||
|
return int($self->volumes->[$i]->select_group_id / 1000000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
sub mouse_event {
|
sub mouse_event {
|
||||||
my ($self, $e) = @_;
|
my ($self, $e) = @_;
|
||||||
|
|
||||||
|
@ -191,9 +215,33 @@ sub mouse_event {
|
||||||
# If user pressed left or right button we first check whether this happened
|
# If user pressed left or right button we first check whether this happened
|
||||||
# on a volume or not.
|
# on a volume or not.
|
||||||
my $volume_idx = $self->_hover_volume_idx // -1;
|
my $volume_idx = $self->_hover_volume_idx // -1;
|
||||||
|
$self->_layer_height_edited(0);
|
||||||
|
if ($self->layer_editing_enabled && $self->{print}) {
|
||||||
|
my $object_idx_selected = $self->_first_selected_object_id;
|
||||||
|
if ($object_idx_selected != -1) {
|
||||||
|
# A volume is selected. Test, whether hovering over a layer thickness bar.
|
||||||
|
my ($cw, $ch) = $self->GetSizeWH;
|
||||||
|
my $bar_width = 70;
|
||||||
|
if ($e->GetX >= $cw - $bar_width) {
|
||||||
|
# Start editing the layer height.
|
||||||
|
$self->_layer_height_edited(1);
|
||||||
|
my $z = unscale($self->{print}->get_object($object_idx_selected)->size->z) * ($ch - $e->GetY - 1.) / ($ch - 1);
|
||||||
|
# print "Modifying height profile at $z\n";
|
||||||
|
# $self->{print}->get_object($object_idx_selected)->adjust_layer_height_profile($z, $e->RightDown ? - 0.05 : 0.05, 2., 0);
|
||||||
|
$self->{print}->get_object($object_idx_selected)->generate_layer_height_texture(
|
||||||
|
$self->volumes->[$object_idx_selected]->layer_height_texture_data->ptr,
|
||||||
|
$self->{layer_preview_z_texture_height},
|
||||||
|
$self->{layer_preview_z_texture_width});
|
||||||
|
$self->Refresh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# select volume in this 3D canvas
|
if (! $self->_layer_height_edited) {
|
||||||
if ($self->enable_picking) {
|
# Select volume in this 3D canvas.
|
||||||
|
# Don't deselect a volume if layer editing is enabled. We want the object to stay selected
|
||||||
|
# during the scene manipulation.
|
||||||
|
if ($self->enable_picking && ($volume_idx != -1 || ! $self->layer_editing_enabled)) {
|
||||||
$self->deselect_volumes;
|
$self->deselect_volumes;
|
||||||
$self->select_volume($volume_idx);
|
$self->select_volume($volume_idx);
|
||||||
|
|
||||||
|
@ -224,7 +272,8 @@ sub mouse_event {
|
||||||
if $self->on_right_click;
|
if $self->on_right_click;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} elsif ($e->Dragging && $e->LeftIsDown && defined($self->_drag_volume_idx)) {
|
}
|
||||||
|
} elsif ($e->Dragging && $e->LeftIsDown && ! $self->_layer_height_edited && defined($self->_drag_volume_idx)) {
|
||||||
# get new position at the same Z of the initial click point
|
# get new position at the same Z of the initial click point
|
||||||
my $mouse_ray = $self->mouse_ray($e->GetX, $e->GetY);
|
my $mouse_ray = $self->mouse_ray($e->GetX, $e->GetY);
|
||||||
my $cur_pos = $mouse_ray->intersect_plane($self->_drag_start_pos->z);
|
my $cur_pos = $mouse_ray->intersect_plane($self->_drag_start_pos->z);
|
||||||
|
@ -249,14 +298,29 @@ sub mouse_event {
|
||||||
$self->_dragged(1);
|
$self->_dragged(1);
|
||||||
$self->Refresh;
|
$self->Refresh;
|
||||||
} elsif ($e->Dragging) {
|
} elsif ($e->Dragging) {
|
||||||
if ($e->LeftIsDown) {
|
if ($self->_layer_height_edited) {
|
||||||
|
my $object_idx_selected = $self->_first_selected_object_id;
|
||||||
|
if ($object_idx_selected != -1) {
|
||||||
|
# A volume is selected. Test, whether hovering over a layer thickness bar.
|
||||||
|
my ($cw, $ch) = $self->GetSizeWH;
|
||||||
|
my $z = unscale($self->{print}->get_object($object_idx_selected)->size->z) * ($ch - $e->GetY - 1.) / ($ch - 1);
|
||||||
|
# print "Modifying height profile at $z\n";
|
||||||
|
my $strength = 0.005;
|
||||||
|
$self->{print}->get_object($object_idx_selected)->adjust_layer_height_profile($z, $e->RightIsDown ? - $strength : $strength, 2., $e->ShiftDown ? 1 : 0);
|
||||||
|
$self->{print}->get_object($object_idx_selected)->generate_layer_height_texture(
|
||||||
|
$self->volumes->[$object_idx_selected]->layer_height_texture_data->ptr,
|
||||||
|
$self->{layer_preview_z_texture_height},
|
||||||
|
$self->{layer_preview_z_texture_width});
|
||||||
|
$self->Refresh;
|
||||||
|
}
|
||||||
|
} elsif ($e->LeftIsDown) {
|
||||||
# if dragging over blank area with left button, rotate
|
# if dragging over blank area with left button, rotate
|
||||||
if (defined $self->_drag_start_pos) {
|
if (defined $self->_drag_start_pos) {
|
||||||
my $orig = $self->_drag_start_pos;
|
my $orig = $self->_drag_start_pos;
|
||||||
if (TURNTABLE_MODE) {
|
if (TURNTABLE_MODE) {
|
||||||
$self->_sphi($self->_sphi + ($pos->x - $orig->x) * TRACKBALLSIZE);
|
$self->_sphi($self->_sphi + ($pos->x - $orig->x) * TRACKBALLSIZE);
|
||||||
$self->_stheta($self->_stheta - ($pos->y - $orig->y) * TRACKBALLSIZE); #-
|
$self->_stheta($self->_stheta - ($pos->y - $orig->y) * TRACKBALLSIZE); #-
|
||||||
$self->_stheta(GIMBAL_LOCK_THETA_MAX) if $self->_stheta > GIMBAL_LOCK_THETA_MAX;
|
$self->_stheta(GIMBALL_LOCK_THETA_MAX) if $self->_stheta > GIMBALL_LOCK_THETA_MAX;
|
||||||
$self->_stheta(0) if $self->_stheta < 0;
|
$self->_stheta(0) if $self->_stheta < 0;
|
||||||
} else {
|
} else {
|
||||||
my $size = $self->GetClientSize;
|
my $size = $self->GetClientSize;
|
||||||
|
@ -304,6 +368,7 @@ sub mouse_event {
|
||||||
$self->_drag_start_pos(undef);
|
$self->_drag_start_pos(undef);
|
||||||
$self->_drag_start_xy(undef);
|
$self->_drag_start_xy(undef);
|
||||||
$self->_dragged(undef);
|
$self->_dragged(undef);
|
||||||
|
$self->_layer_height_edited(undef);
|
||||||
} elsif ($e->Moving) {
|
} elsif ($e->Moving) {
|
||||||
$self->_mouse_pos($pos);
|
$self->_mouse_pos($pos);
|
||||||
$self->Refresh;
|
$self->Refresh;
|
||||||
|
@ -340,8 +405,8 @@ sub select_view {
|
||||||
if (ref($direction)) {
|
if (ref($direction)) {
|
||||||
$dirvec = $direction;
|
$dirvec = $direction;
|
||||||
} else {
|
} else {
|
||||||
if ($direction eq 'iso') {
|
if ($direction eq 'default') {
|
||||||
$dirvec = VIEW_ISO;
|
$dirvec = VIEW_DEFAULT;
|
||||||
} elsif ($direction eq 'left') {
|
} elsif ($direction eq 'left') {
|
||||||
$dirvec = VIEW_LEFT;
|
$dirvec = VIEW_LEFT;
|
||||||
} elsif ($direction eq 'right') {
|
} elsif ($direction eq 'right') {
|
||||||
|
@ -356,22 +421,18 @@ sub select_view {
|
||||||
$dirvec = VIEW_REAR;
|
$dirvec = VIEW_REAR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
my $bb = $self->volumes_bounding_box;
|
||||||
|
if (! $bb->empty) {
|
||||||
$self->_sphi($dirvec->[0]);
|
$self->_sphi($dirvec->[0]);
|
||||||
$self->_stheta($dirvec->[1]);
|
$self->_stheta($dirvec->[1]);
|
||||||
|
# Avoid gimball lock.
|
||||||
# Avoid gimbal lock.
|
$self->_stheta(GIMBALL_LOCK_THETA_MAX) if $self->_stheta > GIMBALL_LOCK_THETA_MAX;
|
||||||
$self->_stheta(GIMBAL_LOCK_THETA_MAX) if $self->_stheta > GIMBAL_LOCK_THETA_MAX;
|
|
||||||
$self->_stheta(0) if $self->_stheta < 0;
|
$self->_stheta(0) if $self->_stheta < 0;
|
||||||
|
|
||||||
# View everything.
|
# View everything.
|
||||||
$self->volumes_bounding_box->defined
|
$self->zoom_to_bounding_box($bb);
|
||||||
? $self->zoom_to_volumes
|
|
||||||
: $self->zoom_to_bed;
|
|
||||||
|
|
||||||
$self->on_viewport_changed->() if $self->on_viewport_changed;
|
$self->on_viewport_changed->() if $self->on_viewport_changed;
|
||||||
$self->_dirty(1);
|
|
||||||
$self->Refresh;
|
$self->Refresh;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sub zoom_to_bounding_box {
|
sub zoom_to_bounding_box {
|
||||||
|
@ -380,7 +441,7 @@ sub zoom_to_bounding_box {
|
||||||
|
|
||||||
# calculate the zoom factor needed to adjust viewport to
|
# calculate the zoom factor needed to adjust viewport to
|
||||||
# bounding box
|
# bounding box
|
||||||
my $max_size = max(@{$bb->size}) * 1.05;
|
my $max_size = max(@{$bb->size}) * 2;
|
||||||
my $min_viewport_size = min($self->GetSizeWH);
|
my $min_viewport_size = min($self->GetSizeWH);
|
||||||
$self->_zoom($min_viewport_size / $max_size);
|
$self->_zoom($min_viewport_size / $max_size);
|
||||||
|
|
||||||
|
@ -725,10 +786,17 @@ sub InitGL {
|
||||||
$self->init(1);
|
$self->init(1);
|
||||||
|
|
||||||
my $shader;
|
my $shader;
|
||||||
# $shader = $self->{shader} = new Slic3r::GUI::GLShader;
|
$shader = $self->{shader} = new Slic3r::GUI::GLShader;
|
||||||
if ($self->{shader}) {
|
if ($self->{shader}) {
|
||||||
my $info = $shader->Load($self->_fragment_shader, $self->_vertex_shader);
|
my $info = $shader->Load($self->_fragment_shader, $self->_vertex_shader);
|
||||||
print $info if $info;
|
print $info if $info;
|
||||||
|
|
||||||
|
($self->{layer_preview_z_texture_id}) = glGenTextures_p(1);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, $self->{layer_preview_z_texture_id});
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
glClearColor(0, 0, 0, 1);
|
glClearColor(0, 0, 0, 1);
|
||||||
|
@ -809,10 +877,14 @@ sub Render {
|
||||||
glLightfv_p(GL_LIGHT0, GL_SPECULAR, 0.2, 0.2, 0.2, 1);
|
glLightfv_p(GL_LIGHT0, GL_SPECULAR, 0.2, 0.2, 0.2, 1);
|
||||||
glLightfv_p(GL_LIGHT0, GL_DIFFUSE, 0.5, 0.5, 0.5, 1);
|
glLightfv_p(GL_LIGHT0, GL_DIFFUSE, 0.5, 0.5, 0.5, 1);
|
||||||
|
|
||||||
|
# Head light
|
||||||
|
glLightfv_p(GL_LIGHT1, GL_POSITION, 1, 0, 1, 0);
|
||||||
|
|
||||||
if ($self->enable_picking) {
|
if ($self->enable_picking) {
|
||||||
# Render the object for picking.
|
# Render the object for picking.
|
||||||
# FIXME This cannot possibly work in a multi-sampled context as the color gets mangled by the anti-aliasing.
|
# FIXME This cannot possibly work in a multi-sampled context as the color gets mangled by the anti-aliasing.
|
||||||
# Better to use software ray-casting on a bounding-box hierarchy.
|
# Better to use software ray-casting on a bounding-box hierarchy.
|
||||||
|
glDisable(GL_MULTISAMPLE);
|
||||||
glDisable(GL_LIGHTING);
|
glDisable(GL_LIGHTING);
|
||||||
$self->draw_volumes(1);
|
$self->draw_volumes(1);
|
||||||
glFlush();
|
glFlush();
|
||||||
|
@ -839,6 +911,7 @@ sub Render {
|
||||||
glFlush();
|
glFlush();
|
||||||
glFinish();
|
glFinish();
|
||||||
glEnable(GL_LIGHTING);
|
glEnable(GL_LIGHTING);
|
||||||
|
glEnable(GL_MULTISAMPLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
# draw fixed background
|
# draw fixed background
|
||||||
|
@ -958,6 +1031,8 @@ sub Render {
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$self->draw_active_object_annotations;
|
||||||
|
|
||||||
glFlush();
|
glFlush();
|
||||||
|
|
||||||
$self->SwapBuffers();
|
$self->SwapBuffers();
|
||||||
|
@ -967,18 +1042,64 @@ sub draw_volumes {
|
||||||
# $fakecolor is a boolean indicating, that the objects shall be rendered in a color coding the object index for picking.
|
# $fakecolor is a boolean indicating, that the objects shall be rendered in a color coding the object index for picking.
|
||||||
my ($self, $fakecolor) = @_;
|
my ($self, $fakecolor) = @_;
|
||||||
|
|
||||||
$self->{shader}->Enable if (! $fakecolor && $self->{shader});
|
|
||||||
|
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
glEnableClientState(GL_VERTEX_ARRAY);
|
glEnableClientState(GL_VERTEX_ARRAY);
|
||||||
glEnableClientState(GL_NORMAL_ARRAY);
|
glEnableClientState(GL_NORMAL_ARRAY);
|
||||||
|
|
||||||
|
# The viewport and camera are set to complete view and glOrtho(-$x/2, $x/2, -$y/2, $y/2, -$depth, $depth),
|
||||||
|
# where x, y is the window size divided by $self->_zoom.
|
||||||
|
my ($cw, $ch) = $self->GetSizeWH;
|
||||||
|
my $bar_width = 70;
|
||||||
|
my ($bar_left, $bar_right) = ((0.5 * $cw - $bar_width)/$self->_zoom, $cw/(2*$self->_zoom));
|
||||||
|
my ($bar_bottom, $bar_top) = (-$ch/(2*$self->_zoom), $ch/(2*$self->_zoom));
|
||||||
|
my $mouse_pos = $self->ScreenToClientPoint(Wx::GetMousePosition());
|
||||||
|
my $z_cursor_relative = ($mouse_pos->x < $cw - $bar_width) ? -1000. :
|
||||||
|
($ch - $mouse_pos->y - 1.) / ($ch - 1);
|
||||||
|
|
||||||
foreach my $volume_idx (0..$#{$self->volumes}) {
|
foreach my $volume_idx (0..$#{$self->volumes}) {
|
||||||
my $volume = $self->volumes->[$volume_idx];
|
my $volume = $self->volumes->[$volume_idx];
|
||||||
|
|
||||||
if ($fakecolor) {
|
my $shader_active = 0;
|
||||||
|
if ($self->layer_editing_enabled && ! $fakecolor && $volume->selected && $self->{shader} && $volume->{layer_height_texture_data} && $volume->{layer_height_texture_cells}) {
|
||||||
|
$self->{shader}->Enable;
|
||||||
|
my $z_to_texture_row_id = $self->{shader}->Map('z_to_texture_row');
|
||||||
|
my $z_texture_row_to_normalized_id = $self->{shader}->Map('z_texture_row_to_normalized');
|
||||||
|
my $z_cursor_id = $self->{shader}->Map('z_cursor');
|
||||||
|
die if ! defined($z_to_texture_row_id);
|
||||||
|
die if ! defined($z_texture_row_to_normalized_id);
|
||||||
|
die if ! defined($z_cursor_id);
|
||||||
|
my $ncells = $volume->{layer_height_texture_cells};
|
||||||
|
my $z_max = $volume->{bounding_box}->z_max;
|
||||||
|
glUniform1fARB($z_to_texture_row_id, ($ncells - 1) / ($self->{layer_preview_z_texture_width} * $z_max));
|
||||||
|
glUniform1fARB($z_texture_row_to_normalized_id, 1. / $self->{layer_preview_z_texture_height});
|
||||||
|
glUniform1fARB($z_cursor_id, $z_max * $z_cursor_relative);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, $self->{layer_preview_z_texture_id});
|
||||||
|
# glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_LEVEL, 0);
|
||||||
|
# glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
|
||||||
|
if (1) {
|
||||||
|
glTexImage2D_c(GL_TEXTURE_2D, 0, GL_RGBA8, $self->{layer_preview_z_texture_width}, $self->{layer_preview_z_texture_height},
|
||||||
|
0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
|
||||||
|
glTexImage2D_c(GL_TEXTURE_2D, 1, GL_RGBA8, $self->{layer_preview_z_texture_width} / 2, $self->{layer_preview_z_texture_height} / 2,
|
||||||
|
0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
|
||||||
|
# glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
|
# glPixelStorei(GL_UNPACK_ROW_LENGTH, $self->{layer_preview_z_texture_width});
|
||||||
|
glTexSubImage2D_c(GL_TEXTURE_2D, 0, 0, 0, $self->{layer_preview_z_texture_width}, $self->{layer_preview_z_texture_height},
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, $volume->{layer_height_texture_data}->ptr);
|
||||||
|
glTexSubImage2D_c(GL_TEXTURE_2D, 1, 0, 0, $self->{layer_preview_z_texture_width} / 2, $self->{layer_preview_z_texture_height} / 2,
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, $volume->{layer_height_texture_data}->offset($self->{layer_preview_z_texture_width} * $self->{layer_preview_z_texture_height} * 4));
|
||||||
|
} else {
|
||||||
|
glTexImage2D_c(GL_TEXTURE_2D, 0, GL_RGBA8, $self->{layer_preview_z_texture_width}, $self->{layer_preview_z_texture_height},
|
||||||
|
0, GL_RGBA, GL_UNSIGNED_BYTE, $volume->{layer_height_texture_data}->ptr);
|
||||||
|
glTexImage2D_c(GL_TEXTURE_2D, 1, GL_RGBA8, $self->{layer_preview_z_texture_width}/2, $self->{layer_preview_z_texture_height}/2,
|
||||||
|
0, GL_RGBA, GL_UNSIGNED_BYTE, $volume->{layer_height_texture_data}->ptr + $self->{layer_preview_z_texture_width} * $self->{layer_preview_z_texture_height} * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
# my $nlines = ceil($ncells / ($self->{layer_preview_z_texture_width} - 1));
|
||||||
|
|
||||||
|
$shader_active = 1;
|
||||||
|
} elsif ($fakecolor) {
|
||||||
# Object picking mode. Render the object with a color encoding the object index.
|
# Object picking mode. Render the object with a color encoding the object index.
|
||||||
my $r = ($volume_idx & 0x000000FF) >> 0;
|
my $r = ($volume_idx & 0x000000FF) >> 0;
|
||||||
my $g = ($volume_idx & 0x0000FF00) >> 8;
|
my $g = ($volume_idx & 0x0000FF00) >> 8;
|
||||||
|
@ -1057,6 +1178,11 @@ sub draw_volumes {
|
||||||
glVertexPointer_c(3, GL_FLOAT, 0, 0);
|
glVertexPointer_c(3, GL_FLOAT, 0, 0);
|
||||||
glNormalPointer_c(GL_FLOAT, 0, 0);
|
glNormalPointer_c(GL_FLOAT, 0, 0);
|
||||||
glPopMatrix();
|
glPopMatrix();
|
||||||
|
|
||||||
|
if ($shader_active) {
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
$self->{shader}->Disable;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
glDisableClientState(GL_NORMAL_ARRAY);
|
glDisableClientState(GL_NORMAL_ARRAY);
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
|
@ -1069,8 +1195,93 @@ sub draw_volumes {
|
||||||
glVertexPointer_c(3, GL_FLOAT, 0, 0);
|
glVertexPointer_c(3, GL_FLOAT, 0, 0);
|
||||||
}
|
}
|
||||||
glDisableClientState(GL_VERTEX_ARRAY);
|
glDisableClientState(GL_VERTEX_ARRAY);
|
||||||
|
}
|
||||||
|
|
||||||
$self->{shader}->Disable if (! $fakecolor && $self->{shader});
|
sub draw_active_object_annotations {
|
||||||
|
# $fakecolor is a boolean indicating, that the objects shall be rendered in a color coding the object index for picking.
|
||||||
|
my ($self) = @_;
|
||||||
|
|
||||||
|
return if (! $self->{shader} || ! $self->layer_editing_enabled);
|
||||||
|
|
||||||
|
my $volume;
|
||||||
|
foreach my $volume_idx (0..$#{$self->volumes}) {
|
||||||
|
my $v = $self->volumes->[$volume_idx];
|
||||||
|
if ($v->selected && $v->{layer_height_texture_data} && $v->{layer_height_texture_cells}) {
|
||||||
|
$volume = $v;
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (! $volume);
|
||||||
|
|
||||||
|
# The viewport and camera are set to complete view and glOrtho(-$x/2, $x/2, -$y/2, $y/2, -$depth, $depth),
|
||||||
|
# where x, y is the window size divided by $self->_zoom.
|
||||||
|
my ($cw, $ch) = $self->GetSizeWH;
|
||||||
|
my $bar_width = 70;
|
||||||
|
my ($bar_left, $bar_right) = ((0.5 * $cw - $bar_width)/$self->_zoom, $cw/(2*$self->_zoom));
|
||||||
|
my ($bar_bottom, $bar_top) = (-$ch/(2*$self->_zoom), $ch/(2*$self->_zoom));
|
||||||
|
my $mouse_pos = $self->ScreenToClientPoint(Wx::GetMousePosition());
|
||||||
|
my $z_cursor_relative = ($mouse_pos->x < $cw - $bar_width) ? -1000. :
|
||||||
|
($ch - $mouse_pos->y - 1.) / ($ch - 1);
|
||||||
|
|
||||||
|
$self->{shader}->Enable;
|
||||||
|
my $z_to_texture_row_id = $self->{shader}->Map('z_to_texture_row');
|
||||||
|
my $z_texture_row_to_normalized_id = $self->{shader}->Map('z_texture_row_to_normalized');
|
||||||
|
my $z_cursor_id = $self->{shader}->Map('z_cursor');
|
||||||
|
my $ncells = $volume->{layer_height_texture_cells};
|
||||||
|
my $z_max = $volume->{bounding_box}->z_max;
|
||||||
|
glUniform1fARB($z_to_texture_row_id, ($ncells - 1) / ($self->{layer_preview_z_texture_width} * $z_max));
|
||||||
|
glUniform1fARB($z_texture_row_to_normalized_id, 1. / $self->{layer_preview_z_texture_height});
|
||||||
|
glUniform1fARB($z_cursor_id, $z_max * $z_cursor_relative);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, $self->{layer_preview_z_texture_id});
|
||||||
|
glTexImage2D_c(GL_TEXTURE_2D, 0, GL_RGBA8, $self->{layer_preview_z_texture_width}, $self->{layer_preview_z_texture_height},
|
||||||
|
0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
|
||||||
|
glTexImage2D_c(GL_TEXTURE_2D, 1, GL_RGBA8, $self->{layer_preview_z_texture_width} / 2, $self->{layer_preview_z_texture_height} / 2,
|
||||||
|
0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
|
||||||
|
glTexSubImage2D_c(GL_TEXTURE_2D, 0, 0, 0, $self->{layer_preview_z_texture_width}, $self->{layer_preview_z_texture_height},
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, $volume->{layer_height_texture_data}->ptr);
|
||||||
|
glTexSubImage2D_c(GL_TEXTURE_2D, 1, 0, 0, $self->{layer_preview_z_texture_width} / 2, $self->{layer_preview_z_texture_height} / 2,
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, $volume->{layer_height_texture_data}->offset($self->{layer_preview_z_texture_width} * $self->{layer_preview_z_texture_height} * 4));
|
||||||
|
|
||||||
|
# Render the color bar.
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
# The viewport and camera are set to complete view and glOrtho(-$x/2, $x/2, -$y/2, $y/2, -$depth, $depth),
|
||||||
|
# where x, y is the window size divided by $self->_zoom.
|
||||||
|
glPushMatrix();
|
||||||
|
glLoadIdentity();
|
||||||
|
# Paint the overlay.
|
||||||
|
glBegin(GL_QUADS);
|
||||||
|
glVertex3f($bar_left, $bar_bottom, 0);
|
||||||
|
glVertex3f($bar_right, $bar_bottom, 0);
|
||||||
|
glVertex3f($bar_right, $bar_top, $volume->{bounding_box}->z_max);
|
||||||
|
glVertex3f($bar_left, $bar_top, $volume->{bounding_box}->z_max);
|
||||||
|
glEnd();
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
$self->{shader}->Disable;
|
||||||
|
|
||||||
|
# Paint the graph.
|
||||||
|
my $object_idx = int($volume->select_group_id / 1000000);
|
||||||
|
my $print_object = $self->{print}->get_object($object_idx);
|
||||||
|
my $max_z = unscale($print_object->size->z);
|
||||||
|
my $profile = $print_object->layer_height_profile;
|
||||||
|
my $layer_height = $print_object->config->get('layer_height');
|
||||||
|
# Baseline
|
||||||
|
glColor3f(0., 0., 0.);
|
||||||
|
glBegin(GL_LINE_STRIP);
|
||||||
|
glVertex2f($bar_left + $layer_height * ($bar_right - $bar_left) / 0.45, $bar_bottom);
|
||||||
|
glVertex2f($bar_left + $layer_height * ($bar_right - $bar_left) / 0.45, $bar_top);
|
||||||
|
glEnd();
|
||||||
|
# Curve
|
||||||
|
glColor3f(0., 0., 1.);
|
||||||
|
glBegin(GL_LINE_STRIP);
|
||||||
|
for (my $i = 0; $i < int(@{$profile}); $i += 2) {
|
||||||
|
my $z = $profile->[$i];
|
||||||
|
my $h = $profile->[$i+1];
|
||||||
|
glVertex3f($bar_left + $h * ($bar_right - $bar_left) / 0.45, $bar_bottom + $z * ($bar_top - $bar_bottom) / $max_z, $z);
|
||||||
|
}
|
||||||
|
glEnd();
|
||||||
|
# Revert the matrices.
|
||||||
|
glPopMatrix();
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
sub _report_opengl_state
|
sub _report_opengl_state
|
||||||
|
@ -1106,57 +1317,61 @@ sub _vertex_shader {
|
||||||
return <<'VERTEX';
|
return <<'VERTEX';
|
||||||
#version 110
|
#version 110
|
||||||
|
|
||||||
|
#define LIGHT_TOP_DIR 0., 1., 0.
|
||||||
|
#define LIGHT_TOP_DIFFUSE 0.2
|
||||||
|
#define LIGHT_TOP_SPECULAR 0.3
|
||||||
|
|
||||||
|
#define LIGHT_FRONT_DIR 0., 0., 1.
|
||||||
|
#define LIGHT_FRONT_DIFFUSE 0.5
|
||||||
|
#define LIGHT_FRONT_SPECULAR 0.3
|
||||||
|
|
||||||
|
#define INTENSITY_AMBIENT 0.1
|
||||||
|
|
||||||
|
uniform float z_to_texture_row;
|
||||||
|
varying float intensity_specular;
|
||||||
|
varying float intensity_tainted;
|
||||||
varying float object_z;
|
varying float object_z;
|
||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
vec3 normal, lightDir, viewVector, halfVector;
|
vec3 eye, normal, lightDir, viewVector, halfVector;
|
||||||
vec4 diffuse, ambient, globalAmbient, specular = vec4(0.0);
|
float NdotL, NdotHV;
|
||||||
float NdotL,NdotHV;
|
|
||||||
|
// eye = gl_ModelViewMatrixInverse[3].xyz;
|
||||||
|
eye = vec3(0., 0., 1.);
|
||||||
|
|
||||||
// First transform the normal into eye space and normalize the result.
|
// First transform the normal into eye space and normalize the result.
|
||||||
normal = normalize(gl_NormalMatrix * gl_Normal);
|
normal = normalize(gl_NormalMatrix * gl_Normal);
|
||||||
|
|
||||||
// Now normalize the light's direction. Note that according to the OpenGL specification, the light is stored in eye space.
|
// Now normalize the light's direction. Note that according to the OpenGL specification, the light is stored in eye space.
|
||||||
// Also since we're talking about a directional light, the position field is actually direction.
|
// Also since we're talking about a directional light, the position field is actually direction.
|
||||||
lightDir = normalize(vec3(gl_LightSource[0].position));
|
lightDir = vec3(LIGHT_TOP_DIR);
|
||||||
|
halfVector = normalize(lightDir + eye);
|
||||||
|
|
||||||
// Compute the cos of the angle between the normal and lights direction. The light is directional so the direction is constant for every vertex.
|
// Compute the cos of the angle between the normal and lights direction. The light is directional so the direction is constant for every vertex.
|
||||||
// Since these two are normalized the cosine is the dot product. We also need to clamp the result to the [0,1] range.
|
// Since these two are normalized the cosine is the dot product. We also need to clamp the result to the [0,1] range.
|
||||||
NdotL = max(dot(normal, lightDir), 0.0);
|
NdotL = max(dot(normal, lightDir), 0.0);
|
||||||
|
|
||||||
// Compute the diffuse, ambient and globalAmbient terms.
|
intensity_tainted = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE;
|
||||||
// diffuse = NdotL * (gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse);
|
intensity_specular = 0.;
|
||||||
// ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;
|
|
||||||
diffuse = NdotL * (gl_Color * gl_LightSource[0].diffuse);
|
|
||||||
ambient = gl_Color * gl_LightSource[0].ambient;
|
|
||||||
globalAmbient = gl_LightModel.ambient * gl_FrontMaterial.ambient;
|
|
||||||
|
|
||||||
// compute the specular term if NdotL is larger than zero
|
// if (NdotL > 0.0)
|
||||||
if (NdotL > 0.0) {
|
// intensity_specular = LIGHT_TOP_SPECULAR * pow(max(dot(normal, halfVector), 0.0), gl_FrontMaterial.shininess);
|
||||||
NdotHV = max(dot(normal, normalize(gl_LightSource[0].halfVector.xyz)),0.0);
|
|
||||||
specular = gl_FrontMaterial.specular * gl_LightSource[0].specular * pow(NdotHV,gl_FrontMaterial.shininess);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the same lighting calculation for the 2nd light source.
|
// Perform the same lighting calculation for the 2nd light source.
|
||||||
lightDir = normalize(vec3(gl_LightSource[1].position));
|
lightDir = vec3(LIGHT_FRONT_DIR);
|
||||||
|
halfVector = normalize(lightDir + eye);
|
||||||
NdotL = max(dot(normal, lightDir), 0.0);
|
NdotL = max(dot(normal, lightDir), 0.0);
|
||||||
// diffuse += NdotL * (gl_FrontMaterial.diffuse * gl_LightSource[1].diffuse);
|
intensity_tainted += NdotL * LIGHT_FRONT_DIFFUSE;
|
||||||
// ambient += gl_FrontMaterial.ambient * gl_LightSource[1].ambient;
|
|
||||||
diffuse += NdotL * (gl_Color * gl_LightSource[1].diffuse);
|
|
||||||
ambient += gl_Color * gl_LightSource[1].ambient;
|
|
||||||
|
|
||||||
// compute the specular term if NdotL is larger than zero
|
// compute the specular term if NdotL is larger than zero
|
||||||
if (NdotL > 0.0) {
|
if (NdotL > 0.0)
|
||||||
NdotHV = max(dot(normal, normalize(gl_LightSource[1].halfVector.xyz)),0.0);
|
intensity_specular += LIGHT_FRONT_SPECULAR * pow(max(dot(normal, halfVector), 0.0), gl_FrontMaterial.shininess);
|
||||||
specular += gl_FrontMaterial.specular * gl_LightSource[1].specular * pow(NdotHV,gl_FrontMaterial.shininess);
|
|
||||||
}
|
|
||||||
|
|
||||||
gl_FrontColor = globalAmbient + diffuse + ambient + specular;
|
// Scaled to widths of the Z texture.
|
||||||
gl_FrontColor.a = 1.;
|
object_z = gl_Vertex.z / gl_Vertex.w;
|
||||||
|
|
||||||
gl_Position = ftransform();
|
gl_Position = ftransform();
|
||||||
object_z = gl_Vertex.z / gl_Vertex.w;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VERTEX
|
VERTEX
|
||||||
|
@ -1165,17 +1380,39 @@ VERTEX
|
||||||
sub _fragment_shader {
|
sub _fragment_shader {
|
||||||
return <<'FRAGMENT';
|
return <<'FRAGMENT';
|
||||||
#version 110
|
#version 110
|
||||||
|
|
||||||
#define M_PI 3.1415926535897932384626433832795
|
#define M_PI 3.1415926535897932384626433832795
|
||||||
|
|
||||||
|
// 2D texture (1D texture split by the rows) of color along the object Z axis.
|
||||||
|
uniform sampler2D z_texture;
|
||||||
|
// Scaling from the Z texture rows coordinate to the normalized texture row coordinate.
|
||||||
|
uniform float z_to_texture_row;
|
||||||
|
uniform float z_texture_row_to_normalized;
|
||||||
|
|
||||||
|
varying float intensity_specular;
|
||||||
|
varying float intensity_tainted;
|
||||||
varying float object_z;
|
varying float object_z;
|
||||||
|
uniform float z_cursor;
|
||||||
|
uniform float z_cursor_band_width;
|
||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
float layer_height = 0.25;
|
float object_z_row = z_to_texture_row * object_z;
|
||||||
float layer_height2 = 0.5 * layer_height;
|
// Index of the row in the texture.
|
||||||
float layer_center = floor(object_z / layer_height) * layer_height + layer_height2;
|
float z_texture_row = floor(object_z_row);
|
||||||
float intensity = cos(M_PI * 0.7 * (layer_center - object_z) / layer_height);
|
// Normalized coordinate from 0. to 1.
|
||||||
gl_FragColor = gl_Color * intensity;
|
float z_texture_col = object_z_row - z_texture_row;
|
||||||
|
// float z_blend = 0.5 + 0.5 * cos(min(M_PI, abs(M_PI * (object_z - z_cursor) / 3.)));
|
||||||
|
// float z_blend = 0.5 * cos(min(M_PI, abs(M_PI * (object_z - z_cursor)))) + 0.5;
|
||||||
|
float z_blend = 0.25 * cos(min(M_PI, abs(M_PI * (object_z - z_cursor)))) + 0.25;
|
||||||
|
// Scale z_texture_row to normalized coordinates.
|
||||||
|
// Sample the Z texture.
|
||||||
|
gl_FragColor =
|
||||||
|
vec4(intensity_specular, intensity_specular, intensity_specular, 1.) +
|
||||||
|
// intensity_tainted * texture2D(z_texture, vec2(z_texture_col, z_texture_row_to_normalized * (z_texture_row + 0.5)), -2.5);
|
||||||
|
(1. - z_blend) * intensity_tainted * texture2D(z_texture, vec2(z_texture_col, z_texture_row_to_normalized * (z_texture_row + 0.5)), -200.) +
|
||||||
|
z_blend * vec4(1., 1., 0., 0.);
|
||||||
|
// and reset the transparency.
|
||||||
gl_FragColor.a = 1.;
|
gl_FragColor.a = 1.;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1189,6 +1426,8 @@ use Moo;
|
||||||
has 'bounding_box' => (is => 'ro', required => 1);
|
has 'bounding_box' => (is => 'ro', required => 1);
|
||||||
has 'origin' => (is => 'rw', default => sub { Slic3r::Pointf3->new(0,0,0) });
|
has 'origin' => (is => 'rw', default => sub { Slic3r::Pointf3->new(0,0,0) });
|
||||||
has 'color' => (is => 'ro', required => 1);
|
has 'color' => (is => 'ro', required => 1);
|
||||||
|
# An ID containing the object ID, volume ID and instance ID.
|
||||||
|
has 'composite_id' => (is => 'rw', default => sub { -1 });
|
||||||
# An ID for group selection. It may be the same for all meshes of all object instances, or for just a single object instance.
|
# An ID for group selection. It may be the same for all meshes of all object instances, or for just a single object instance.
|
||||||
has 'select_group_id' => (is => 'rw', default => sub { -1 });
|
has 'select_group_id' => (is => 'rw', default => sub { -1 });
|
||||||
# An ID for group dragging. It may be the same for all meshes of all object instances, or for just a single object instance.
|
# An ID for group dragging. It may be the same for all meshes of all object instances, or for just a single object instance.
|
||||||
|
@ -1210,6 +1449,26 @@ has 'tverts' => (is => 'rw');
|
||||||
# The offsets stores tripples of (z_top, qverts_idx, tverts_idx) in a linear array.
|
# The offsets stores tripples of (z_top, qverts_idx, tverts_idx) in a linear array.
|
||||||
has 'offsets' => (is => 'rw');
|
has 'offsets' => (is => 'rw');
|
||||||
|
|
||||||
|
# RGBA texture along the Z axis of an object, to visualize layers by stripes colored by their height.
|
||||||
|
has 'layer_height_texture_data' => (is => 'rw');
|
||||||
|
# Number of texture cells.
|
||||||
|
has 'layer_height_texture_cells' => (is => 'rw');
|
||||||
|
|
||||||
|
sub object_idx {
|
||||||
|
my ($self) = @_;
|
||||||
|
return $self->composite_id / 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub volume_idx {
|
||||||
|
my ($self) = @_;
|
||||||
|
return ($self->composite_id / 1000) % 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub instance_idx {
|
||||||
|
my ($self) = @_;
|
||||||
|
return $self->composite_id % 1000;
|
||||||
|
}
|
||||||
|
|
||||||
sub transformed_bounding_box {
|
sub transformed_bounding_box {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
|
|
||||||
|
@ -1235,8 +1494,6 @@ __PACKAGE__->mk_accessors(qw(
|
||||||
color_by
|
color_by
|
||||||
select_by
|
select_by
|
||||||
drag_by
|
drag_by
|
||||||
volumes_by_object
|
|
||||||
_objects_by_volumes
|
|
||||||
));
|
));
|
||||||
|
|
||||||
sub new {
|
sub new {
|
||||||
|
@ -1246,14 +1503,12 @@ sub new {
|
||||||
$self->color_by('volume'); # object | volume
|
$self->color_by('volume'); # object | volume
|
||||||
$self->select_by('object'); # object | volume | instance
|
$self->select_by('object'); # object | volume | instance
|
||||||
$self->drag_by('instance'); # object | instance
|
$self->drag_by('instance'); # object | instance
|
||||||
$self->volumes_by_object({}); # obj_idx => [ volume_idx, volume_idx ... ]
|
|
||||||
$self->_objects_by_volumes({}); # volume_idx => [ obj_idx, instance_idx ]
|
|
||||||
|
|
||||||
return $self;
|
return $self;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub load_object {
|
sub load_object {
|
||||||
my ($self, $model, $obj_idx, $instance_idxs) = @_;
|
my ($self, $model, $print, $obj_idx, $instance_idxs) = @_;
|
||||||
|
|
||||||
my $model_object;
|
my $model_object;
|
||||||
if ($model->isa('Slic3r::Model::Object')) {
|
if ($model->isa('Slic3r::Model::Object')) {
|
||||||
|
@ -1266,6 +1521,19 @@ sub load_object {
|
||||||
|
|
||||||
$instance_idxs ||= [0..$#{$model_object->instances}];
|
$instance_idxs ||= [0..$#{$model_object->instances}];
|
||||||
|
|
||||||
|
# Object will have a single common layer height texture for all volumes.
|
||||||
|
my $layer_height_texture_data;
|
||||||
|
my $layer_height_texture_cells;
|
||||||
|
if ($print && $obj_idx < $print->object_count) {
|
||||||
|
# Generate the layer height texture. Allocate data for the 0th and 1st mipmap levels.
|
||||||
|
$layer_height_texture_data = OpenGL::Array->new($self->{layer_preview_z_texture_width}*$self->{layer_preview_z_texture_height}*5, GL_UNSIGNED_BYTE);
|
||||||
|
# $print->get_object($obj_idx)->update_layer_height_profile_from_ranges();
|
||||||
|
$layer_height_texture_cells = $print->get_object($obj_idx)->generate_layer_height_texture(
|
||||||
|
$layer_height_texture_data->ptr,
|
||||||
|
$self->{layer_preview_z_texture_height},
|
||||||
|
$self->{layer_preview_z_texture_width});
|
||||||
|
}
|
||||||
|
|
||||||
my @volumes_idx = ();
|
my @volumes_idx = ();
|
||||||
foreach my $volume_idx (0..$#{$model_object->volumes}) {
|
foreach my $volume_idx (0..$#{$model_object->volumes}) {
|
||||||
my $volume = $model_object->volumes->[$volume_idx];
|
my $volume = $model_object->volumes->[$volume_idx];
|
||||||
|
@ -1287,16 +1555,18 @@ sub load_object {
|
||||||
# not correspond to the color of the filament.
|
# not correspond to the color of the filament.
|
||||||
my $color = [ @{COLORS->[ $color_idx % scalar(@{&COLORS}) ]} ];
|
my $color = [ @{COLORS->[ $color_idx % scalar(@{&COLORS}) ]} ];
|
||||||
$color->[3] = $volume->modifier ? 0.5 : 1;
|
$color->[3] = $volume->modifier ? 0.5 : 1;
|
||||||
|
print "Reloading object $volume_idx, $instance_idx\n";
|
||||||
push @{$self->volumes}, my $v = Slic3r::GUI::3DScene::Volume->new(
|
push @{$self->volumes}, my $v = Slic3r::GUI::3DScene::Volume->new(
|
||||||
bounding_box => $mesh->bounding_box,
|
bounding_box => $mesh->bounding_box,
|
||||||
color => $color,
|
color => $color,
|
||||||
);
|
);
|
||||||
|
$v->composite_id($obj_idx*1000000 + $volume_idx*1000 + $instance_idx);
|
||||||
if ($self->select_by eq 'object') {
|
if ($self->select_by eq 'object') {
|
||||||
$v->select_group_id($obj_idx*1000000);
|
$v->select_group_id($obj_idx*1000000);
|
||||||
} elsif ($self->select_by eq 'volume') {
|
} elsif ($self->select_by eq 'volume') {
|
||||||
$v->select_group_id($obj_idx*1000000 + $volume_idx*1000);
|
$v->select_group_id($obj_idx*1000000 + $volume_idx*1000);
|
||||||
} elsif ($self->select_by eq 'instance') {
|
} elsif ($self->select_by eq 'instance') {
|
||||||
$v->select_group_id($obj_idx*1000000 + $volume_idx*1000 + $instance_idx);
|
$v->select_group_id($v->composite_id);
|
||||||
}
|
}
|
||||||
if ($self->drag_by eq 'object') {
|
if ($self->drag_by eq 'object') {
|
||||||
$v->drag_group_id($obj_idx*1000);
|
$v->drag_group_id($obj_idx*1000);
|
||||||
|
@ -1304,15 +1574,18 @@ sub load_object {
|
||||||
$v->drag_group_id($obj_idx*1000 + $instance_idx);
|
$v->drag_group_id($obj_idx*1000 + $instance_idx);
|
||||||
}
|
}
|
||||||
push @volumes_idx, my $scene_volume_idx = $#{$self->volumes};
|
push @volumes_idx, my $scene_volume_idx = $#{$self->volumes};
|
||||||
$self->_objects_by_volumes->{$scene_volume_idx} = [ $obj_idx, $volume_idx, $instance_idx ];
|
|
||||||
|
|
||||||
my $verts = Slic3r::GUI::_3DScene::GLVertexArray->new;
|
my $verts = Slic3r::GUI::_3DScene::GLVertexArray->new;
|
||||||
$verts->load_mesh($mesh);
|
$verts->load_mesh($mesh);
|
||||||
$v->tverts($verts);
|
$v->tverts($verts);
|
||||||
|
|
||||||
|
if (! $volume->modifier) {
|
||||||
|
$v->layer_height_texture_data($layer_height_texture_data);
|
||||||
|
$v->layer_height_texture_cells($layer_height_texture_cells);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$self->volumes_by_object->{$obj_idx} = [@volumes_idx];
|
|
||||||
return @volumes_idx;
|
return @volumes_idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1651,19 +1924,4 @@ sub _extrusionentity_to_verts {
|
||||||
$tverts);
|
$tverts);
|
||||||
}
|
}
|
||||||
|
|
||||||
sub object_idx {
|
|
||||||
my ($self, $volume_idx) = @_;
|
|
||||||
return $self->_objects_by_volumes->{$volume_idx}[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
sub volume_idx {
|
|
||||||
my ($self, $volume_idx) = @_;
|
|
||||||
return $self->_objects_by_volumes->{$volume_idx}[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
sub instance_idx {
|
|
||||||
my ($self, $volume_idx) = @_;
|
|
||||||
return $self->_objects_by_volumes->{$volume_idx}[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|
|
@ -8,10 +8,11 @@ use utf8;
|
||||||
use File::Basename qw(basename dirname);
|
use File::Basename qw(basename dirname);
|
||||||
use List::Util qw(sum first max);
|
use List::Util qw(sum first max);
|
||||||
use Slic3r::Geometry qw(X Y Z MIN MAX scale unscale deg2rad);
|
use Slic3r::Geometry qw(X Y Z MIN MAX scale unscale deg2rad);
|
||||||
|
use LWP::UserAgent;
|
||||||
use threads::shared qw(shared_clone);
|
use threads::shared qw(shared_clone);
|
||||||
use Wx qw(:button :cursor :dialog :filedialog :keycode :icon :font :id :listctrl :misc
|
use Wx qw(:button :cursor :dialog :filedialog :keycode :icon :font :id :listctrl :misc
|
||||||
:panel :sizer :toolbar :window wxTheApp :notebook :combobox);
|
:panel :sizer :toolbar :window wxTheApp :notebook :combobox);
|
||||||
use Wx::Event qw(EVT_BUTTON EVT_COMMAND EVT_KEY_DOWN EVT_LIST_ITEM_ACTIVATED
|
use Wx::Event qw(EVT_BUTTON EVT_TOGGLEBUTTON EVT_COMMAND EVT_KEY_DOWN EVT_LIST_ITEM_ACTIVATED
|
||||||
EVT_LIST_ITEM_DESELECTED EVT_LIST_ITEM_SELECTED EVT_MOUSE_EVENTS EVT_PAINT EVT_TOOL
|
EVT_LIST_ITEM_DESELECTED EVT_LIST_ITEM_SELECTED EVT_MOUSE_EVENTS EVT_PAINT EVT_TOOL
|
||||||
EVT_CHOICE EVT_COMBOBOX EVT_TIMER EVT_NOTEBOOK_PAGE_CHANGED);
|
EVT_CHOICE EVT_COMBOBOX EVT_TIMER EVT_NOTEBOOK_PAGE_CHANGED);
|
||||||
use base 'Wx::Panel';
|
use base 'Wx::Panel';
|
||||||
|
@ -30,6 +31,7 @@ use constant TB_SCALE => &Wx::NewId;
|
||||||
use constant TB_SPLIT => &Wx::NewId;
|
use constant TB_SPLIT => &Wx::NewId;
|
||||||
use constant TB_CUT => &Wx::NewId;
|
use constant TB_CUT => &Wx::NewId;
|
||||||
use constant TB_SETTINGS => &Wx::NewId;
|
use constant TB_SETTINGS => &Wx::NewId;
|
||||||
|
use constant TB_LAYER_EDITING => &Wx::NewId;
|
||||||
|
|
||||||
# package variables to avoid passing lexicals to threads
|
# package variables to avoid passing lexicals to threads
|
||||||
our $THUMBNAIL_DONE_EVENT : shared = Wx::NewEventType;
|
our $THUMBNAIL_DONE_EVENT : shared = Wx::NewEventType;
|
||||||
|
@ -94,7 +96,7 @@ sub new {
|
||||||
|
|
||||||
# Initialize 3D plater
|
# Initialize 3D plater
|
||||||
if ($Slic3r::GUI::have_OpenGL) {
|
if ($Slic3r::GUI::have_OpenGL) {
|
||||||
$self->{canvas3D} = Slic3r::GUI::Plater::3D->new($self->{preview_notebook}, $self->{objects}, $self->{model}, $self->{config});
|
$self->{canvas3D} = Slic3r::GUI::Plater::3D->new($self->{preview_notebook}, $self->{objects}, $self->{model}, $self->{print}, $self->{config});
|
||||||
$self->{preview_notebook}->AddPage($self->{canvas3D}, '3D');
|
$self->{preview_notebook}->AddPage($self->{canvas3D}, '3D');
|
||||||
$self->{canvas3D}->set_on_select_object($on_select_object);
|
$self->{canvas3D}->set_on_select_object($on_select_object);
|
||||||
$self->{canvas3D}->set_on_double_click($on_double_click);
|
$self->{canvas3D}->set_on_double_click($on_double_click);
|
||||||
|
@ -154,6 +156,9 @@ sub new {
|
||||||
$self->{htoolbar}->AddTool(TB_CUT, "Cut…", Wx::Bitmap->new($Slic3r::var->("package.png"), wxBITMAP_TYPE_PNG), '');
|
$self->{htoolbar}->AddTool(TB_CUT, "Cut…", Wx::Bitmap->new($Slic3r::var->("package.png"), wxBITMAP_TYPE_PNG), '');
|
||||||
$self->{htoolbar}->AddSeparator;
|
$self->{htoolbar}->AddSeparator;
|
||||||
$self->{htoolbar}->AddTool(TB_SETTINGS, "Settings…", Wx::Bitmap->new($Slic3r::var->("cog.png"), wxBITMAP_TYPE_PNG), '');
|
$self->{htoolbar}->AddTool(TB_SETTINGS, "Settings…", Wx::Bitmap->new($Slic3r::var->("cog.png"), wxBITMAP_TYPE_PNG), '');
|
||||||
|
|
||||||
|
# FIXME add a button for layer editing
|
||||||
|
$self->{htoolbar}->AddCheckTool(TB_LAYER_EDITING, "Layer editing", Wx::Bitmap->new($Slic3r::var->("cog.png"), wxBITMAP_TYPE_PNG), '');
|
||||||
} else {
|
} else {
|
||||||
my %tbar_buttons = (
|
my %tbar_buttons = (
|
||||||
add => "Add…",
|
add => "Add…",
|
||||||
|
@ -168,12 +173,15 @@ sub new {
|
||||||
split => "Split",
|
split => "Split",
|
||||||
cut => "Cut…",
|
cut => "Cut…",
|
||||||
settings => "Settings…",
|
settings => "Settings…",
|
||||||
|
layer_editing => "Layer editing",
|
||||||
);
|
);
|
||||||
$self->{btoolbar} = Wx::BoxSizer->new(wxHORIZONTAL);
|
$self->{btoolbar} = Wx::BoxSizer->new(wxHORIZONTAL);
|
||||||
for (qw(add remove reset arrange increase decrease rotate45ccw rotate45cw changescale split cut settings)) {
|
for (qw(add remove reset arrange increase decrease rotate45ccw rotate45cw changescale split cut settings)) {
|
||||||
$self->{"btn_$_"} = Wx::Button->new($self, -1, $tbar_buttons{$_}, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT);
|
$self->{"btn_$_"} = Wx::Button->new($self, -1, $tbar_buttons{$_}, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT);
|
||||||
$self->{btoolbar}->Add($self->{"btn_$_"});
|
$self->{btoolbar}->Add($self->{"btn_$_"});
|
||||||
}
|
}
|
||||||
|
$self->{"btn_layer_editing"} = Wx::ToggleButton->new($self, -1, $tbar_buttons{'layer_editing'}, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT);
|
||||||
|
$self->{btoolbar}->Add($self->{"btn_layer_editing"});
|
||||||
}
|
}
|
||||||
|
|
||||||
$self->{list} = Wx::ListView->new($self, -1, wxDefaultPosition, wxDefaultSize,
|
$self->{list} = Wx::ListView->new($self, -1, wxDefaultPosition, wxDefaultSize,
|
||||||
|
@ -256,6 +264,7 @@ sub new {
|
||||||
EVT_TOOL($self, TB_SPLIT, sub { $self->split_object; });
|
EVT_TOOL($self, TB_SPLIT, sub { $self->split_object; });
|
||||||
EVT_TOOL($self, TB_CUT, sub { $_[0]->object_cut_dialog });
|
EVT_TOOL($self, TB_CUT, sub { $_[0]->object_cut_dialog });
|
||||||
EVT_TOOL($self, TB_SETTINGS, sub { $_[0]->object_settings_dialog });
|
EVT_TOOL($self, TB_SETTINGS, sub { $_[0]->object_settings_dialog });
|
||||||
|
EVT_TOOL($self, TB_LAYER_EDITING, sub { $self->on_layer_editing_toggled($self->{htoolbar}->GetToolState(TB_LAYER_EDITING)); });
|
||||||
} else {
|
} else {
|
||||||
EVT_BUTTON($self, $self->{btn_add}, sub { $self->add; });
|
EVT_BUTTON($self, $self->{btn_add}, sub { $self->add; });
|
||||||
EVT_BUTTON($self, $self->{btn_remove}, sub { $self->remove() }); # explicitly pass no argument to remove
|
EVT_BUTTON($self, $self->{btn_remove}, sub { $self->remove() }); # explicitly pass no argument to remove
|
||||||
|
@ -269,6 +278,7 @@ sub new {
|
||||||
EVT_BUTTON($self, $self->{btn_split}, sub { $self->split_object; });
|
EVT_BUTTON($self, $self->{btn_split}, sub { $self->split_object; });
|
||||||
EVT_BUTTON($self, $self->{btn_cut}, sub { $_[0]->object_cut_dialog });
|
EVT_BUTTON($self, $self->{btn_cut}, sub { $_[0]->object_cut_dialog });
|
||||||
EVT_BUTTON($self, $self->{btn_settings}, sub { $_[0]->object_settings_dialog });
|
EVT_BUTTON($self, $self->{btn_settings}, sub { $_[0]->object_settings_dialog });
|
||||||
|
EVT_TOGGLEBUTTON($self, $self->{btn_layer_editing}, sub { $self->on_layer_editing_toggled($self->{btn_layer_editing}->GetValue); });
|
||||||
}
|
}
|
||||||
|
|
||||||
$_->SetDropTarget(Slic3r::GUI::Plater::DropTarget->new($self))
|
$_->SetDropTarget(Slic3r::GUI::Plater::DropTarget->new($self))
|
||||||
|
@ -464,6 +474,13 @@ sub _on_select_preset {
|
||||||
$self->on_config_change($self->GetFrame->config);
|
$self->on_config_change($self->GetFrame->config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub on_layer_editing_toggled {
|
||||||
|
my ($self, $new_state) = @_;
|
||||||
|
print "on_layer_editing_toggled $new_state\n";
|
||||||
|
$self->{canvas3D}->layer_editing_enabled($new_state);
|
||||||
|
$self->{canvas3D}->update;
|
||||||
|
}
|
||||||
|
|
||||||
sub GetFrame {
|
sub GetFrame {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
return &Wx::GetTopLevelParent($self);
|
return &Wx::GetTopLevelParent($self);
|
||||||
|
@ -1482,6 +1499,8 @@ sub on_thumbnail_made {
|
||||||
sub update {
|
sub update {
|
||||||
my ($self, $force_autocenter) = @_;
|
my ($self, $force_autocenter) = @_;
|
||||||
|
|
||||||
|
print "Platter - update\n";
|
||||||
|
|
||||||
if ($Slic3r::GUI::Settings->{_}{autocenter} || $force_autocenter) {
|
if ($Slic3r::GUI::Settings->{_}{autocenter} || $force_autocenter) {
|
||||||
$self->{model}->center_instances_around_point($self->bed_centerf);
|
$self->{model}->center_instances_around_point($self->bed_centerf);
|
||||||
}
|
}
|
||||||
|
@ -1679,7 +1698,7 @@ sub object_list_changed {
|
||||||
my $have_objects = @{$self->{objects}} ? 1 : 0;
|
my $have_objects = @{$self->{objects}} ? 1 : 0;
|
||||||
my $method = $have_objects ? 'Enable' : 'Disable';
|
my $method = $have_objects ? 'Enable' : 'Disable';
|
||||||
$self->{"btn_$_"}->$method
|
$self->{"btn_$_"}->$method
|
||||||
for grep $self->{"btn_$_"}, qw(reset arrange reslice export_gcode export_stl print send_gcode);
|
for grep $self->{"btn_$_"}, qw(reset arrange reslice export_gcode export_stl print send_gcode layer_editing);
|
||||||
|
|
||||||
if ($self->{export_gcode_output_file} || $self->{send_gcode_file}) {
|
if ($self->{export_gcode_output_file} || $self->{send_gcode_file}) {
|
||||||
$self->{btn_reslice}->Disable;
|
$self->{btn_reslice}->Disable;
|
||||||
|
@ -1712,6 +1731,7 @@ sub selection_changed {
|
||||||
if ($self->{object_info_size}) { # have we already loaded the info pane?
|
if ($self->{object_info_size}) { # have we already loaded the info pane?
|
||||||
if ($have_sel) {
|
if ($have_sel) {
|
||||||
my $model_object = $self->{model}->objects->[$obj_idx];
|
my $model_object = $self->{model}->objects->[$obj_idx];
|
||||||
|
$model_object->print_info;
|
||||||
my $model_instance = $model_object->instances->[0];
|
my $model_instance = $model_object->instances->[0];
|
||||||
$self->{object_info_size}->SetLabel(sprintf("%.2f x %.2f x %.2f", @{$model_object->instance_bounding_box(0)->size}));
|
$self->{object_info_size}->SetLabel(sprintf("%.2f x %.2f x %.2f", @{$model_object->instance_bounding_box(0)->size}));
|
||||||
$self->{object_info_materials}->SetLabel($model_object->materials_count);
|
$self->{object_info_materials}->SetLabel($model_object->materials_count);
|
||||||
|
|
|
@ -12,7 +12,7 @@ use base qw(Slic3r::GUI::3DScene Class::Accessor);
|
||||||
|
|
||||||
sub new {
|
sub new {
|
||||||
my $class = shift;
|
my $class = shift;
|
||||||
my ($parent, $objects, $model, $config) = @_;
|
my ($parent, $objects, $model, $print, $config) = @_;
|
||||||
|
|
||||||
my $self = $class->SUPER::new($parent);
|
my $self = $class->SUPER::new($parent);
|
||||||
$self->enable_picking(1);
|
$self->enable_picking(1);
|
||||||
|
@ -22,6 +22,7 @@ sub new {
|
||||||
|
|
||||||
$self->{objects} = $objects;
|
$self->{objects} = $objects;
|
||||||
$self->{model} = $model;
|
$self->{model} = $model;
|
||||||
|
$self->{print} = $print;
|
||||||
$self->{config} = $config;
|
$self->{config} = $config;
|
||||||
$self->{on_select_object} = sub {};
|
$self->{on_select_object} = sub {};
|
||||||
$self->{on_instances_moved} = sub {};
|
$self->{on_instances_moved} = sub {};
|
||||||
|
@ -31,7 +32,7 @@ sub new {
|
||||||
|
|
||||||
my $obj_idx = undef;
|
my $obj_idx = undef;
|
||||||
if ($volume_idx != -1) {
|
if ($volume_idx != -1) {
|
||||||
$obj_idx = $self->object_idx($volume_idx);
|
$obj_idx = $self->volumes->[$volume_idx]->object_idx;
|
||||||
}
|
}
|
||||||
$self->{on_select_object}->($obj_idx)
|
$self->{on_select_object}->($obj_idx)
|
||||||
if $self->{on_select_object};
|
if $self->{on_select_object};
|
||||||
|
@ -42,8 +43,8 @@ sub new {
|
||||||
my %done = (); # prevent moving instances twice
|
my %done = (); # prevent moving instances twice
|
||||||
foreach my $volume_idx (@volume_idxs) {
|
foreach my $volume_idx (@volume_idxs) {
|
||||||
my $volume = $self->volumes->[$volume_idx];
|
my $volume = $self->volumes->[$volume_idx];
|
||||||
my $obj_idx = $self->object_idx($volume_idx);
|
my $obj_idx = $volume->object_idx;
|
||||||
my $instance_idx = $self->instance_idx($volume_idx);
|
my $instance_idx = $volume->instance_idx;
|
||||||
next if $done{"${obj_idx}_${instance_idx}"};
|
next if $done{"${obj_idx}_${instance_idx}"};
|
||||||
$done{"${obj_idx}_${instance_idx}"} = 1;
|
$done{"${obj_idx}_${instance_idx}"} = 1;
|
||||||
|
|
||||||
|
@ -89,7 +90,7 @@ sub update {
|
||||||
$self->update_bed_size;
|
$self->update_bed_size;
|
||||||
|
|
||||||
foreach my $obj_idx (0..$#{$self->{model}->objects}) {
|
foreach my $obj_idx (0..$#{$self->{model}->objects}) {
|
||||||
my @volume_idxs = $self->load_object($self->{model}, $obj_idx);
|
my @volume_idxs = $self->load_object($self->{model}, $self->{print}, $obj_idx);
|
||||||
|
|
||||||
if ($self->{objects}[$obj_idx]->selected) {
|
if ($self->{objects}[$obj_idx]->selected) {
|
||||||
$self->select_volume($_) for @volume_idxs;
|
$self->select_volume($_) for @volume_idxs;
|
||||||
|
|
|
@ -114,7 +114,7 @@ sub new {
|
||||||
my $canvas;
|
my $canvas;
|
||||||
if ($Slic3r::GUI::have_OpenGL) {
|
if ($Slic3r::GUI::have_OpenGL) {
|
||||||
$canvas = $self->{canvas} = Slic3r::GUI::3DScene->new($self);
|
$canvas = $self->{canvas} = Slic3r::GUI::3DScene->new($self);
|
||||||
$canvas->load_object($self->{model_object}, undef, [0]);
|
$canvas->load_object($self->{model_object}, undef, undef, [0]);
|
||||||
$canvas->set_auto_bed_shape;
|
$canvas->set_auto_bed_shape;
|
||||||
$canvas->SetSize([500,500]);
|
$canvas->SetSize([500,500]);
|
||||||
$canvas->SetMinSize($canvas->GetSize);
|
$canvas->SetMinSize($canvas->GetSize);
|
||||||
|
@ -244,7 +244,7 @@ sub _update {
|
||||||
}
|
}
|
||||||
|
|
||||||
$self->{canvas}->reset_objects;
|
$self->{canvas}->reset_objects;
|
||||||
$self->{canvas}->load_object($_, undef, [0]) for @objects;
|
$self->{canvas}->load_object($_, undef, undef, [0]) for @objects;
|
||||||
$self->{canvas}->SetCuttingPlane(
|
$self->{canvas}->SetCuttingPlane(
|
||||||
$self->{cut_options}{z},
|
$self->{cut_options}{z},
|
||||||
[@expolygons],
|
[@expolygons],
|
||||||
|
|
|
@ -22,6 +22,7 @@ sub new {
|
||||||
my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
|
my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
|
||||||
|
|
||||||
my $object = $self->{model_object} = $params{model_object};
|
my $object = $self->{model_object} = $params{model_object};
|
||||||
|
my $print_object = $self->{print_object} = $params{print_object};
|
||||||
|
|
||||||
# create TreeCtrl
|
# create TreeCtrl
|
||||||
my $tree = $self->{tree} = Wx::TreeCtrl->new($self, -1, wxDefaultPosition, [300, 100],
|
my $tree = $self->{tree} = Wx::TreeCtrl->new($self, -1, wxDefaultPosition, [300, 100],
|
||||||
|
@ -82,7 +83,7 @@ sub new {
|
||||||
$self->reload_tree($canvas->volume_idx($volume_idx));
|
$self->reload_tree($canvas->volume_idx($volume_idx));
|
||||||
});
|
});
|
||||||
|
|
||||||
$canvas->load_object($self->{model_object}, undef, [0]);
|
$canvas->load_object($self->{model_object}, undef, undef, [0]);
|
||||||
$canvas->set_auto_bed_shape;
|
$canvas->set_auto_bed_shape;
|
||||||
$canvas->SetSize([500,500]);
|
$canvas->SetSize([500,500]);
|
||||||
$canvas->zoom_to_volumes;
|
$canvas->zoom_to_volumes;
|
||||||
|
|
Loading…
Add table
Reference in a new issue