Model.pm 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  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. sub read_from_file {
  8. my $class = shift;
  9. my ($input_file) = @_;
  10. my $model = $input_file =~ /\.stl$/i ? Slic3r::Format::STL->read_file($input_file)
  11. : $input_file =~ /\.obj$/i ? Slic3r::Format::OBJ->read_file($input_file)
  12. : $input_file =~ /\.amf(\.xml)?$/i ? Slic3r::Format::AMF->read_file($input_file)
  13. : die "Input file must have .stl, .obj or .amf(.xml) extension\n";
  14. $_->input_file($input_file) for @{$model->objects};
  15. return $model;
  16. }
  17. sub merge {
  18. my $class = shift;
  19. my @models = @_;
  20. my $new_model = ref($class)
  21. ? $class
  22. : $class->new;
  23. $new_model->add_object($_) for map @{$_->objects}, @models;
  24. return $new_model;
  25. }
  26. sub add_object {
  27. my $self = shift;
  28. my $new_object;
  29. if (@_ == 1) {
  30. # we have a Model::Object
  31. my ($object) = @_;
  32. $new_object = $self->add_object(
  33. input_file => $object->input_file,
  34. config => $object->config,
  35. layer_height_ranges => $object->layer_height_ranges, # TODO: clone!
  36. );
  37. foreach my $volume (@{$object->volumes}) {
  38. $new_object->add_volume($volume);
  39. }
  40. $new_object->add_instance(
  41. offset => $_->offset,
  42. rotation => $_->rotation,
  43. scaling_factor => $_->scaling_factor,
  44. ) for @{ $object->instances // [] };
  45. } else {
  46. push @{$self->objects}, $new_object = Slic3r::Model::Object->new(model => $self, @_);
  47. }
  48. return $new_object;
  49. }
  50. sub delete_object {
  51. my ($self, $obj_idx) = @_;
  52. splice @{$self->objects}, $obj_idx, 1;
  53. }
  54. sub delete_all_objects {
  55. my ($self) = @_;
  56. @{$self->objects} = ();
  57. }
  58. sub set_material {
  59. my $self = shift;
  60. my ($material_id, $attributes) = @_;
  61. return $self->materials->{$material_id} = Slic3r::Model::Material->new(
  62. model => $self,
  63. attributes => $attributes || {},
  64. );
  65. }
  66. sub duplicate_objects_grid {
  67. my ($self, $grid, $distance) = @_;
  68. die "Grid duplication is not supported with multiple objects\n"
  69. if @{$self->objects} > 1;
  70. my $object = $self->objects->[0];
  71. @{$object->instances} = ();
  72. my $size = $object->bounding_box->size;
  73. for my $x_copy (1..$grid->[X]) {
  74. for my $y_copy (1..$grid->[Y]) {
  75. $object->add_instance(
  76. offset => [
  77. ($size->[X] + $distance) * ($x_copy-1),
  78. ($size->[Y] + $distance) * ($y_copy-1),
  79. ],
  80. );
  81. }
  82. }
  83. }
  84. # this will append more instances to each object
  85. # and then automatically rearrange everything
  86. sub duplicate_objects {
  87. my ($self, $copies_num, $distance, $bb) = @_;
  88. foreach my $object (@{$self->objects}) {
  89. my @instances = @{$object->instances};
  90. foreach my $instance (@instances) {
  91. ### $object->add_instance($instance->clone); if we had clone()
  92. $object->add_instance(
  93. offset => [ @{$instance->offset} ],
  94. rotation => $instance->rotation,
  95. scaling_factor => $instance->scaling_factor,
  96. ) for 2..$copies_num;
  97. }
  98. }
  99. $self->arrange_objects($distance, $bb);
  100. }
  101. # arrange objects preserving their instance count
  102. # but altering their instance positions
  103. sub arrange_objects {
  104. my ($self, $distance, $bb) = @_;
  105. # get the (transformed) size of each instance so that we take
  106. # into account their different transformations when packing
  107. my @instance_sizes = ();
  108. foreach my $object (@{$self->objects}) {
  109. push @instance_sizes, map $object->instance_bounding_box($_)->size, 0..$#{$object->instances};
  110. }
  111. my @positions = $self->_arrange(\@instance_sizes, $distance, $bb);
  112. foreach my $object (@{$self->objects}) {
  113. $_->offset([ @{shift @positions} ]) for @{$object->instances};
  114. $object->update_bounding_box;
  115. }
  116. }
  117. # duplicate the entire model preserving instance relative positions
  118. sub duplicate {
  119. my ($self, $copies_num, $distance, $bb) = @_;
  120. my $model_size = $self->bounding_box->size;
  121. my @positions = $self->_arrange([ map $model_size, 2..$copies_num ], $distance, $bb);
  122. # note that this will leave the object count unaltered
  123. foreach my $object (@{$self->objects}) {
  124. my @instances = @{$object->instances}; # store separately to avoid recursion from add_instance() below
  125. foreach my $instance (@instances) {
  126. foreach my $pos (@positions) {
  127. ### $object->add_instance($instance->clone); if we had clone()
  128. $object->add_instance(
  129. offset => [ $instance->offset->[X] + $pos->[X], $instance->offset->[Y] + $pos->[Y] ],
  130. rotation => $instance->rotation,
  131. scaling_factor => $instance->scaling_factor,
  132. );
  133. }
  134. }
  135. $object->update_bounding_box;
  136. }
  137. }
  138. sub _arrange {
  139. my ($self, $sizes, $distance, $bb) = @_;
  140. return Slic3r::Geometry::arrange(
  141. scalar(@$sizes), # number of parts
  142. max(map $_->x, @$sizes), # cell width
  143. max(map $_->y, @$sizes), # cell height ,
  144. $distance, # distance between cells
  145. $bb, # bounding box of the area to fill (can be undef)
  146. );
  147. }
  148. sub has_objects_with_no_instances {
  149. my ($self) = @_;
  150. return (first { !defined $_->instances } @{$self->objects}) ? 1 : 0;
  151. }
  152. # this returns the bounding box of the *transformed* instances
  153. sub bounding_box {
  154. my $self = shift;
  155. return undef if !@{$self->objects};
  156. my $bb = $self->objects->[0]->bounding_box;
  157. $bb->merge($_->bounding_box) for @{$self->objects}[1..$#{$self->objects}];
  158. return $bb;
  159. }
  160. # input point is expressed in unscaled coordinates
  161. sub center_instances_around_point {
  162. my ($self, $point) = @_;
  163. my $bb = $self->bounding_box;
  164. return if !defined $bb;
  165. my $size = $bb->size;
  166. my @shift = (
  167. -$bb->x_min + $point->[X] - $size->x/2,
  168. -$bb->y_min + $point->[Y] - $size->y/2, #//
  169. );
  170. foreach my $object (@{$self->objects}) {
  171. foreach my $instance (@{$object->instances}) {
  172. $instance->offset->[X] += $shift[X];
  173. $instance->offset->[Y] += $shift[Y];
  174. }
  175. $object->update_bounding_box;
  176. }
  177. }
  178. sub align_instances_to_origin {
  179. my ($self) = @_;
  180. my $bb = $self->bounding_box;
  181. return if !defined $bb;
  182. my $new_center = $bb->size;
  183. $new_center->translate(-$new_center->x/2, -$new_center->y/2); #//
  184. $self->center_instances_around_point($new_center);
  185. }
  186. sub translate {
  187. my $self = shift;
  188. my @shift = @_;
  189. $_->translate(@shift) for @{$self->objects};
  190. }
  191. # flattens everything to a single mesh
  192. sub mesh {
  193. my $self = shift;
  194. my $mesh = Slic3r::TriangleMesh->new;
  195. $mesh->merge($_->mesh) for @{$self->objects};
  196. return $mesh;
  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}) {
  212. my $new_object = $self->add_object(
  213. input_file => $object->input_file,
  214. config => $object->config->clone,
  215. layer_height_ranges => $object->layer_height_ranges, # TODO: this needs to be cloned
  216. );
  217. $new_object->add_volume(
  218. mesh => $mesh,
  219. material_id => $volume->material_id,
  220. );
  221. # add one instance per original instance
  222. $new_object->add_instance(
  223. offset => [ @{$_->offset} ],
  224. rotation => $_->rotation,
  225. scaling_factor => $_->scaling_factor,
  226. ) for @{ $object->instances // [] };
  227. }
  228. }
  229. }
  230. sub print_info {
  231. my $self = shift;
  232. $_->print_info for @{$self->objects};
  233. }
  234. sub get_material_name {
  235. my $self = shift;
  236. my ($material_id) = @_;
  237. my $name;
  238. if (exists $self->materials->{$material_id}) {
  239. $name //= $self->materials->{$material_id}->attributes->{$_} for qw(Name name);
  240. }
  241. $name //= $material_id;
  242. return $name;
  243. }
  244. package Slic3r::Model::Material;
  245. use Moo;
  246. has 'model' => (is => 'ro', weak_ref => 1, required => 1);
  247. has 'attributes' => (is => 'rw', default => sub { {} });
  248. has 'config' => (is => 'rw', default => sub { Slic3r::Config->new });
  249. package Slic3r::Model::Object;
  250. use Moo;
  251. use File::Basename qw(basename);
  252. use List::Util qw(first sum);
  253. use Slic3r::Geometry qw(X Y Z MIN MAX);
  254. has 'input_file' => (is => 'rw');
  255. has 'model' => (is => 'ro', weak_ref => 1, required => 1);
  256. has 'volumes' => (is => 'ro', default => sub { [] });
  257. has 'instances' => (is => 'rw');
  258. has 'config' => (is => 'rw', default => sub { Slic3r::Config->new });
  259. has 'layer_height_ranges' => (is => 'rw', default => sub { [] }); # [ z_min, z_max, layer_height ]
  260. has '_bounding_box' => (is => 'rw');
  261. has 'origin_translation' => (is => 'ro', default => sub { Slic3r::Point->new }); # translation vector applied by center_around_origin()
  262. sub add_volume {
  263. my $self = shift;
  264. my $new_volume;
  265. if (@_ == 1) {
  266. # we have a Model::Volume
  267. my ($volume) = @_;
  268. $new_volume = Slic3r::Model::Volume->new(
  269. object => $self,
  270. material_id => $volume->material_id,
  271. mesh => $volume->mesh->clone,
  272. modifier => $volume->modifier,
  273. );
  274. if (defined $volume->material_id) {
  275. # merge material attributes and config (should we rename materials in case of duplicates?)
  276. if (my $material = $volume->object->model->materials->{$volume->material_id}) {
  277. my %attributes = %{ $material->attributes };
  278. if (exists $self->model->materials->{$volume->material_id}) {
  279. %attributes = (%attributes, %{ $self->model->materials->{$volume->material_id}->attributes })
  280. }
  281. my $new_material = $self->model->set_material($volume->material_id, {%attributes});
  282. $new_material->config->apply($material->config);
  283. }
  284. }
  285. } else {
  286. my %args = @_;
  287. $new_volume = Slic3r::Model::Volume->new(
  288. object => $self,
  289. %args,
  290. );
  291. }
  292. push @{$self->volumes}, $new_volume;
  293. # invalidate cached bounding box
  294. $self->_bounding_box(undef);
  295. return $new_volume;
  296. }
  297. sub delete_volume {
  298. my ($self, $i) = @_;
  299. splice @{$self->volumes}, $i, 1;
  300. }
  301. sub add_instance {
  302. my $self = shift;
  303. my %params = @_;
  304. $self->instances([]) if !defined $self->instances;
  305. push @{$self->instances}, my $i = Slic3r::Model::Instance->new(object => $self, %params);
  306. $self->_bounding_box(undef);
  307. return $i;
  308. }
  309. sub delete_last_instance {
  310. my ($self) = @_;
  311. pop @{$self->instances};
  312. $self->_bounding_box(undef);
  313. }
  314. sub instances_count {
  315. my $self = shift;
  316. return scalar(@{ $self->instances // [] });
  317. }
  318. sub raw_mesh {
  319. my $self = shift;
  320. my $mesh = Slic3r::TriangleMesh->new;
  321. $mesh->merge($_->mesh) for grep !$_->modifier, @{ $self->volumes };
  322. return $mesh;
  323. }
  324. # flattens all volumes and instances into a single mesh
  325. sub mesh {
  326. my $self = shift;
  327. my $mesh = $self->raw_mesh;
  328. my @instance_meshes = ();
  329. foreach my $instance (@{ $self->instances }) {
  330. my $m = $mesh->clone;
  331. $instance->transform_mesh($m);
  332. push @instance_meshes, $m;
  333. }
  334. my $full_mesh = Slic3r::TriangleMesh->new;
  335. $full_mesh->merge($_) for @instance_meshes;
  336. return $full_mesh;
  337. }
  338. sub update_bounding_box {
  339. my ($self) = @_;
  340. $self->_bounding_box($self->mesh->bounding_box);
  341. }
  342. # this returns the bounding box of the *transformed* instances
  343. sub bounding_box {
  344. my $self = shift;
  345. $self->update_bounding_box if !defined $self->_bounding_box;
  346. return $self->_bounding_box->clone;
  347. }
  348. # this returns the bounding box of the *transformed* given instance
  349. sub instance_bounding_box {
  350. my ($self, $instance_idx) = @_;
  351. my $mesh = $self->raw_mesh;
  352. $self->instances->[$instance_idx]->transform_mesh($mesh);
  353. return $mesh->bounding_box;
  354. }
  355. sub center_around_origin {
  356. my $self = shift;
  357. # calculate the displacements needed to
  358. # center this object around the origin
  359. my $bb = $self->raw_mesh->bounding_box;
  360. # first align to origin on XY
  361. my @shift = (
  362. -$bb->x_min,
  363. -$bb->y_min,
  364. 0,
  365. );
  366. # then center it on XY
  367. my $size = $bb->size;
  368. $shift[X] -= $size->x/2;
  369. $shift[Y] -= $size->y/2; #//
  370. $self->translate(@shift);
  371. $self->origin_translation->translate(@shift[X,Y]);
  372. if (defined $self->instances) {
  373. foreach my $instance (@{ $self->instances }) {
  374. $instance->offset->[X] -= $shift[X];
  375. $instance->offset->[Y] -= $shift[Y];
  376. }
  377. $self->update_bounding_box;
  378. }
  379. return @shift;
  380. }
  381. sub translate {
  382. my $self = shift;
  383. my @shift = @_;
  384. $_->mesh->translate(@shift) for @{$self->volumes};
  385. $self->_bounding_box->translate(@shift) if defined $self->_bounding_box;
  386. }
  387. sub materials_count {
  388. my $self = shift;
  389. my %materials = map { $_->material_id // '_default' => 1 } @{$self->volumes};
  390. return scalar keys %materials;
  391. }
  392. sub unique_materials {
  393. my $self = shift;
  394. my %materials = ();
  395. $materials{ $_->material_id } = 1
  396. for grep { defined $_->material_id } @{$self->volumes};
  397. return sort keys %materials;
  398. }
  399. sub facets_count {
  400. my $self = shift;
  401. return sum(map $_->mesh->facets_count, grep !$_->modifier, @{$self->volumes});
  402. }
  403. sub needed_repair {
  404. my $self = shift;
  405. return (first { !$_->mesh->needed_repair } grep !$_->modifier, @{$self->volumes}) ? 0 : 1;
  406. }
  407. sub mesh_stats {
  408. my $self = shift;
  409. # TODO: sum values from all volumes
  410. return $self->volumes->[0]->mesh->stats;
  411. }
  412. sub print_info {
  413. my $self = shift;
  414. printf "Info about %s:\n", basename($self->input_file);
  415. printf " size: x=%.3f y=%.3f z=%.3f\n", @{$self->raw_mesh->bounding_box->size};
  416. if (my $stats = $self->mesh_stats) {
  417. printf " number of facets: %d\n", $stats->{number_of_facets};
  418. printf " number of shells: %d\n", $stats->{number_of_parts};
  419. printf " volume: %.3f\n", $stats->{volume};
  420. if ($self->needed_repair) {
  421. printf " needed repair: yes\n";
  422. printf " degenerate facets: %d\n", $stats->{degenerate_facets};
  423. printf " edges fixed: %d\n", $stats->{edges_fixed};
  424. printf " facets removed: %d\n", $stats->{facets_removed};
  425. printf " facets added: %d\n", $stats->{facets_added};
  426. printf " facets reversed: %d\n", $stats->{facets_reversed};
  427. printf " backwards edges: %d\n", $stats->{backwards_edges};
  428. } else {
  429. printf " needed repair: no\n";
  430. }
  431. } else {
  432. printf " number of facets: %d\n", scalar(map @{$_->facets}, grep !$_->modifier, @{$self->volumes});
  433. }
  434. }
  435. package Slic3r::Model::Volume;
  436. use Moo;
  437. has 'object' => (is => 'ro', weak_ref => 1, required => 1);
  438. has 'material_id' => (is => 'rw');
  439. has 'mesh' => (is => 'rw', required => 1);
  440. has 'modifier' => (is => 'rw', defualt => sub { 0 });
  441. sub assign_unique_material {
  442. my ($self) = @_;
  443. my $model = $self->object->model;
  444. my $material_id = 1 + scalar keys %{$model->materials};
  445. $self->material_id($material_id);
  446. return $model->set_material($material_id);
  447. }
  448. package Slic3r::Model::Instance;
  449. use Moo;
  450. has 'object' => (is => 'ro', weak_ref => 1, required => 1);
  451. has 'rotation' => (is => 'rw', default => sub { 0 }); # around mesh center point
  452. has 'scaling_factor' => (is => 'rw', default => sub { 1 });
  453. has 'offset' => (is => 'rw'); # must be arrayref in *unscaled* coordinates
  454. sub transform_mesh {
  455. my ($self, $mesh, $dont_translate) = @_;
  456. $mesh->rotate($self->rotation, Slic3r::Point->new(0,0)); # rotate around mesh origin
  457. $mesh->scale($self->scaling_factor); # scale around mesh origin
  458. $mesh->translate(@{$self->offset}, 0) unless $dont_translate;
  459. }
  460. sub transform_polygon {
  461. my ($self, $polygon) = @_;
  462. $polygon->rotate($self->rotation, Slic3r::Point->new(0,0)); # rotate around origin
  463. $polygon->scale($self->scaling_factor); # scale around origin
  464. }
  465. 1;