Model.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. package Slic3r::Model;
  2. use Moo;
  3. use List::Util qw(first max);
  4. use Slic3r::Geometry qw(X Y Z MIN move_points);
  5. has 'materials' => (is => 'ro', default => sub { {} });
  6. has 'objects' => (is => 'ro', default => sub { [] });
  7. has '_bounding_box' => (is => 'rw');
  8. sub read_from_file {
  9. my $class = shift;
  10. my ($input_file) = @_;
  11. my $model = $input_file =~ /\.stl$/i ? Slic3r::Format::STL->read_file($input_file)
  12. : $input_file =~ /\.obj$/i ? Slic3r::Format::OBJ->read_file($input_file)
  13. : $input_file =~ /\.amf(\.xml)?$/i ? Slic3r::Format::AMF->read_file($input_file)
  14. : die "Input file must have .stl, .obj or .amf(.xml) extension\n";
  15. $_->input_file($input_file) for @{$model->objects};
  16. return $model;
  17. }
  18. sub merge {
  19. my $class = shift;
  20. my @models = @_;
  21. my $new_model = $class->new;
  22. foreach my $model (@models) {
  23. # merge material attributes (should we rename them in case of duplicates?)
  24. $new_model->set_material($_, { %{$model->materials->{$_}}, %{$model->materials->{$_} || {}} })
  25. for keys %{$model->materials};
  26. foreach my $object (@{$model->objects}) {
  27. my $new_object = $new_model->add_object(
  28. input_file => $object->input_file,
  29. vertices => $object->vertices,
  30. config => $object->config,
  31. layer_height_ranges => $object->layer_height_ranges,
  32. );
  33. $new_object->add_volume(
  34. material_id => $_->material_id,
  35. facets => $_->facets,
  36. ) for @{$object->volumes};
  37. $new_object->add_instance(
  38. offset => $_->offset,
  39. rotation => $_->rotation,
  40. ) for @{ $object->instances // [] };
  41. }
  42. }
  43. return $new_model;
  44. }
  45. sub add_object {
  46. my $self = shift;
  47. my $object = Slic3r::Model::Object->new(model => $self, @_);
  48. push @{$self->objects}, $object;
  49. $self->_bounding_box(undef);
  50. return $object;
  51. }
  52. sub set_material {
  53. my $self = shift;
  54. my ($material_id, $attributes) = @_;
  55. return $self->materials->{$material_id} = Slic3r::Model::Region->new(
  56. model => $self,
  57. attributes => $attributes || {},
  58. );
  59. }
  60. sub arrange_objects {
  61. my $self = shift;
  62. my ($config) = @_;
  63. # do we have objects with no position?
  64. if (first { !defined $_->instances } @{$self->objects}) {
  65. # we shall redefine positions for all objects
  66. my ($copies, @positions) = $self->_arrange(
  67. config => $config,
  68. items => $self->objects,
  69. );
  70. # apply positions to objects
  71. foreach my $object (@{$self->objects}) {
  72. $object->align_to_origin;
  73. $object->instances([]);
  74. $object->add_instance(
  75. offset => $_,
  76. rotation => 0,
  77. ) for splice @positions, 0, $copies;
  78. }
  79. } else {
  80. # we only have objects with defined position
  81. # align the whole model to origin as it is
  82. $self->align_to_origin;
  83. # arrange this model as a whole
  84. my ($copies, @positions) = $self->_arrange(
  85. config => $config,
  86. items => [$self],
  87. );
  88. # apply positions to objects by translating the current positions
  89. foreach my $object (@{$self->objects}) {
  90. my @old_instances = @{$object->instances};
  91. $object->instances([]);
  92. foreach my $instance (@old_instances) {
  93. $object->add_instance(
  94. offset => $_,
  95. rotation => $instance->rotation,
  96. scaling_factor => $instance->scaling_factor,
  97. ) for move_points($instance->offset, @positions);
  98. }
  99. }
  100. }
  101. }
  102. sub _arrange {
  103. my $self = shift;
  104. my %params = @_;
  105. my $config = $params{config};
  106. my @items = @{$params{items}}; # can be Model or Object objects, they have to implement size()
  107. if ($config->duplicate_grid->[X] > 1 || $config->duplicate_grid->[Y] > 1) {
  108. if (@items > 1) {
  109. die "Grid duplication is not supported with multiple objects\n";
  110. }
  111. my @positions = ();
  112. my $size = $items[0]->size;
  113. my $dist = $config->duplicate_distance;
  114. for my $x_copy (1..$config->duplicate_grid->[X]) {
  115. for my $y_copy (1..$config->duplicate_grid->[Y]) {
  116. push @positions, [
  117. ($size->[X] + $dist) * ($x_copy-1),
  118. ($size->[Y] + $dist) * ($y_copy-1),
  119. ];
  120. }
  121. }
  122. return ($config->duplicate_grid->[X] * $config->duplicate_grid->[Y]), @positions;
  123. } else {
  124. my $total_parts = $config->duplicate * @items;
  125. my @sizes = map $_->size, @items;
  126. my $partx = max(map $_->[X], @sizes);
  127. my $party = max(map $_->[Y], @sizes);
  128. return $config->duplicate,
  129. Slic3r::Geometry::arrange
  130. ($total_parts, $partx, $party, (map $_, @{$config->bed_size}),
  131. $config->min_object_distance, $config);
  132. }
  133. }
  134. sub vertices {
  135. my $self = shift;
  136. return [ map @{$_->vertices}, @{$self->objects} ];
  137. }
  138. sub used_vertices {
  139. my $self = shift;
  140. return [ map @{$_->used_vertices}, @{$self->objects} ];
  141. }
  142. sub size {
  143. my $self = shift;
  144. return $self->bounding_box->size;
  145. }
  146. sub bounding_box {
  147. my $self = shift;
  148. if (!defined $self->_bounding_box) {
  149. $self->_bounding_box(Slic3r::Geometry::BoundingBox->new_from_points_3D($self->used_vertices));
  150. }
  151. return $self->_bounding_box;
  152. }
  153. sub align_to_origin {
  154. my $self = shift;
  155. # calculate the displacements needed to
  156. # have lowest value for each axis at coordinate 0
  157. {
  158. my $bb = $self->bounding_box;
  159. $self->move(map -$bb->extents->[$_][MIN], X,Y,Z);
  160. }
  161. # align all instances to 0,0 as well
  162. {
  163. my @instances = map @{$_->instances}, @{$self->objects};
  164. my @extents = Slic3r::Geometry::bounding_box_3D([ map $_->offset, @instances ]);
  165. $_->offset->translate(-$extents[X][MIN], -$extents[Y][MIN]) for @instances;
  166. }
  167. }
  168. sub scale {
  169. my $self = shift;
  170. $_->scale(@_) for @{$self->objects};
  171. $self->_bounding_box->scale(@_) if defined $self->_bounding_box;
  172. }
  173. sub move {
  174. my $self = shift;
  175. my @shift = @_;
  176. $_->move(@shift) for @{$self->objects};
  177. $self->_bounding_box->translate(@shift) if defined $self->_bounding_box;
  178. }
  179. # flattens everything to a single mesh
  180. sub mesh {
  181. my $self = shift;
  182. my @meshes = ();
  183. foreach my $object (@{$self->objects}) {
  184. my @instances = $object->instances ? @{$object->instances} : (undef);
  185. foreach my $instance (@instances) {
  186. my $mesh = $object->mesh->clone;
  187. if ($instance) {
  188. $mesh->rotate($instance->rotation);
  189. $mesh->scale($instance->scaling_factor);
  190. $mesh->align_to_origin;
  191. $mesh->move(@{$instance->offset});
  192. }
  193. push @meshes, $mesh;
  194. }
  195. }
  196. return Slic3r::TriangleMesh->merge(@meshes);
  197. }
  198. # this method splits objects into multiple distinct objects by walking their meshes
  199. sub split_meshes {
  200. my $self = shift;
  201. my @objects = @{$self->objects};
  202. @{$self->objects} = ();
  203. foreach my $object (@objects) {
  204. if (@{$object->volumes} > 1) {
  205. # We can't split meshes if there's more than one material, because
  206. # we can't group the resulting meshes by object afterwards
  207. push @{$self->objects}, $object;
  208. next;
  209. }
  210. my $volume = $object->volumes->[0];
  211. foreach my $mesh ($volume->mesh->split_mesh) {
  212. my $new_object = $self->add_object(
  213. input_file => $object->input_file,
  214. config => $object->config,
  215. layer_height_ranges => $object->layer_height_ranges,
  216. );
  217. $new_object->add_volume(
  218. vertices => $mesh->vertices,
  219. facets => $mesh->facets,
  220. material_id => $volume->material_id,
  221. );
  222. # let's now align the new object to the origin and put its displacement
  223. # (extents) in the instances info
  224. my $bb = $mesh->bounding_box;
  225. $new_object->align_to_origin;
  226. # add one instance per original instance applying the displacement
  227. $new_object->add_instance(
  228. offset => [ $_->offset->[X] + $bb->x_min, $_->offset->[Y] + $bb->y_min ],
  229. rotation => $_->rotation,
  230. scaling_factor => $_->scaling_factor,
  231. ) for @{ $object->instances // [] };
  232. }
  233. }
  234. }
  235. sub print_info {
  236. my $self = shift;
  237. $_->print_info for @{$self->objects};
  238. }
  239. sub get_material_name {
  240. my $self = shift;
  241. my ($material_id) = @_;
  242. my $name;
  243. if (exists $self->materials->{$material_id}) {
  244. $name //= $self->materials->{$material_id}->attributes->{$_} for qw(Name name);
  245. } elsif ($material_id eq '_') {
  246. $name = 'Default material';
  247. }
  248. $name //= $material_id;
  249. return $name;
  250. }
  251. package Slic3r::Model::Region;
  252. use Moo;
  253. has 'model' => (is => 'ro', weak_ref => 1, required => 1);
  254. has 'attributes' => (is => 'rw', default => sub { {} });
  255. package Slic3r::Model::Object;
  256. use Moo;
  257. use File::Basename qw(basename);
  258. use List::Util qw(first sum);
  259. use Slic3r::Geometry qw(X Y Z MIN MAX move_points move_points_3D);
  260. use Storable qw(dclone);
  261. has 'input_file' => (is => 'rw');
  262. has 'model' => (is => 'ro', weak_ref => 1, required => 1);
  263. has 'vertices' => (is => 'ro', default => sub { [] });
  264. has 'volumes' => (is => 'ro', default => sub { [] });
  265. has 'instances' => (is => 'rw');
  266. has 'config' => (is => 'rw', default => sub { Slic3r::Config->new });
  267. has 'layer_height_ranges' => (is => 'rw', default => sub { [] }); # [ z_min, z_max, layer_height ]
  268. has 'material_mapping' => (is => 'rw', default => sub { {} }); # { material_id => extruder_idx }
  269. has 'mesh_stats' => (is => 'rw');
  270. has '_bounding_box' => (is => 'rw');
  271. sub add_volume {
  272. my $self = shift;
  273. my %args = @_;
  274. if (my $vertices = delete $args{vertices}) {
  275. my $v_offset = @{$self->vertices};
  276. push @{$self->vertices}, @$vertices;
  277. @{$args{facets}} = map {
  278. my $f = [@$_];
  279. $f->[$_] += $v_offset for -3..-1;
  280. $f;
  281. } @{$args{facets}};
  282. }
  283. my $volume = Slic3r::Model::Volume->new(object => $self, %args);
  284. push @{$self->volumes}, $volume;
  285. $self->_bounding_box(undef);
  286. $self->model->_bounding_box(undef);
  287. return $volume;
  288. }
  289. sub add_instance {
  290. my $self = shift;
  291. $self->instances([]) if !defined $self->instances;
  292. push @{$self->instances}, Slic3r::Model::Instance->new(object => $self, @_);
  293. $self->model->_bounding_box(undef);
  294. return $self->instances->[-1];
  295. }
  296. sub mesh {
  297. my $self = shift;
  298. # this mesh won't be suitable for check_manifoldness as multiple
  299. # facets from different volumes may use the same vertices
  300. return Slic3r::TriangleMesh->new(
  301. vertices => $self->vertices,
  302. facets => [ map @{$_->facets}, @{$self->volumes} ],
  303. );
  304. }
  305. sub used_vertices {
  306. my $self = shift;
  307. return [ map $self->vertices->[$_], map @$_, map @{$_->facets}, @{$self->volumes} ];
  308. }
  309. sub size {
  310. my $self = shift;
  311. return $self->bounding_box->size;
  312. }
  313. sub center {
  314. my $self = shift;
  315. return $self->bounding_box->center;
  316. }
  317. sub bounding_box {
  318. my $self = shift;
  319. if (!defined $self->_bounding_box) {
  320. $self->_bounding_box(Slic3r::Geometry::BoundingBox->new_from_points_3D($self->used_vertices));
  321. }
  322. return $self->_bounding_box;
  323. }
  324. sub align_to_origin {
  325. my $self = shift;
  326. # calculate the displacements needed to
  327. # have lowest value for each axis at coordinate 0
  328. my $bb = $self->bounding_box;
  329. my @shift = map -$bb->extents->[$_][MIN], X,Y,Z;
  330. $self->move(@shift);
  331. return @shift;
  332. }
  333. sub move {
  334. my $self = shift;
  335. my @shift = @_;
  336. @{$self->vertices} = move_points_3D([ @shift ], @{$self->vertices});
  337. $self->_bounding_box->translate(@shift) if defined $self->_bounding_box;
  338. }
  339. sub scale {
  340. my $self = shift;
  341. my ($factor) = @_;
  342. return if $factor == 1;
  343. # transform vertex coordinates
  344. foreach my $vertex (@{$self->vertices}) {
  345. $vertex->[$_] *= $factor for X,Y,Z;
  346. }
  347. $self->_bounding_box->scale($factor) if defined $self->_bounding_box;
  348. $self->mesh_stats->{volume} *= ($factor**3) if defined $self->mesh_stats;
  349. }
  350. sub rotate {
  351. my $self = shift;
  352. my ($deg) = @_;
  353. return if $deg == 0;
  354. my $rad = Slic3r::Geometry::deg2rad($deg);
  355. # transform vertex coordinates
  356. foreach my $vertex (@{$self->vertices}) {
  357. @$vertex = (@{ +(Slic3r::Geometry::rotate_points($rad, undef, [ $vertex->[X], $vertex->[Y] ]))[0] }, $vertex->[Z]);
  358. }
  359. $self->_bounding_box(undef);
  360. }
  361. sub materials_count {
  362. my $self = shift;
  363. my %materials = map { $_->material_id // '_default' => 1 } @{$self->volumes};
  364. return scalar keys %materials;
  365. }
  366. sub unique_materials {
  367. my $self = shift;
  368. my %materials = ();
  369. $materials{ $_->material_id // '_' } = 1 for @{$self->volumes};
  370. return sort keys %materials;
  371. }
  372. sub facets_count {
  373. my $self = shift;
  374. return sum(map $_->facets_count, @{$self->volumes});
  375. }
  376. sub check_manifoldness {
  377. my $self = shift;
  378. return (first { !$_->mesh->check_manifoldness } @{$self->volumes}) ? 0 : 1;
  379. }
  380. sub needed_repair {
  381. my $self = shift;
  382. return $self->mesh_stats
  383. && first { $self->mesh_stats->{$_} > 0 }
  384. qw(degenerate_facets edges_fixed facets_removed facets_added facets_reversed backwards_edges);
  385. }
  386. sub print_info {
  387. my $self = shift;
  388. printf "Info about %s:\n", basename($self->input_file);
  389. printf " size: x=%.3f y=%.3f z=%.3f\n", @{$self->size};
  390. if (my $stats = $self->mesh_stats) {
  391. printf " number of facets: %d\n", $stats->{number_of_facets};
  392. printf " number of shells: %d\n", $stats->{number_of_parts};
  393. printf " volume: %.3f\n", $stats->{volume};
  394. if ($self->needed_repair) {
  395. printf " needed repair: yes\n";
  396. printf " degenerate facets: %d\n", $stats->{degenerate_facets};
  397. printf " edges fixed: %d\n", $stats->{edges_fixed};
  398. printf " facets removed: %d\n", $stats->{facets_removed};
  399. printf " facets added: %d\n", $stats->{facets_added};
  400. printf " facets reversed: %d\n", $stats->{facets_reversed};
  401. printf " backwards edges: %d\n", $stats->{backwards_edges};
  402. } else {
  403. printf " needed repair: no\n";
  404. }
  405. } else {
  406. printf " number of facets: %d\n", scalar(map @{$_->facets}, @{$self->volumes});
  407. }
  408. }
  409. sub clone { dclone($_[0]) }
  410. package Slic3r::Model::Volume;
  411. use Moo;
  412. has 'object' => (is => 'ro', weak_ref => 1, required => 1);
  413. has 'material_id' => (is => 'rw');
  414. has 'facets' => (is => 'rw', default => sub { [] });
  415. sub mesh {
  416. my $self = shift;
  417. return Slic3r::TriangleMesh->new(
  418. vertices => $self->object->vertices,
  419. facets => $self->facets,
  420. );
  421. }
  422. sub facets_count {
  423. my $self = shift;
  424. return scalar(@{$self->facets}); # TODO: optimize in XS
  425. }
  426. package Slic3r::Model::Instance;
  427. use Moo;
  428. has 'object' => (is => 'ro', weak_ref => 1, required => 1);
  429. has 'rotation' => (is => 'rw', default => sub { 0 }); # around mesh center point
  430. has 'scaling_factor' => (is => 'rw', default => sub { 1 });
  431. has 'offset' => (is => 'rw'); # must be Slic3r::Point object
  432. 1;