3D Print Toolbox: Add hollow out #105194

Merged
Mikhail Rachinskiy merged 9 commits from usfreitas/blender-addons:hollow into main 2024-03-18 12:24:30 +01:00
Contributor

Hollow out an object with a configurable offset. This is useful in
3D printing workflows, for instance, to save material and/or reduce
printing time. The operator is added to the 3d Print Tools addon, in
the transform section.

Hollow out an object with a configurable offset. This is useful in 3D printing workflows, for instance, to save material and/or reduce printing time. The operator is added to the 3d Print Tools addon, in the transform section.
usfreitas added 1 commit 2024-02-23 22:10:22 +01:00
Hollow out an object with a configurable offset. This is useful in
3D printing workflows, for instance, to save material and/or reduce
printing time. The operator is added to the 3d Print Tools addon, in
the transform section.
usfreitas requested review from Mikhail Rachinskiy 2024-02-23 22:11:07 +01:00

Let's handle all the issues in parts: first we deal with technical issues and formatting, then UI, naming and defaults.

Let's handle all the issues in parts: first we deal with technical issues and formatting, then UI, naming and defaults.
Mikhail Rachinskiy requested changes 2024-02-24 17:57:28 +01:00
Dismissed
Mikhail Rachinskiy left a comment
Member

This one is execution only, next well do UI.

This one is execution only, next well do UI.
@ -821,0 +852,4 @@
def execute(self, context):
import numpy as np
import pyopenvdb as vdb

Ther should be "gate" conditions at the beginning of the execute method checking for invalid values.

if not self.offset:
    return {'FINISHED'}
Ther should be "gate" conditions at the beginning of the execute method checking for invalid values. ``` if not self.offset: return {'FINISHED'} ```
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +855,4 @@
offset = self.offset
resolution = self.resolution
join = self.join

It's better to not create unnecessary local variables, especially when they are rarely used.

It's better to not create unnecessary local variables, especially when they are rarely used.
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +861,4 @@
# Target object
obj = context.active_object
m = obj.data # mesh

Code style

Here m = obj.data # mesh is unnecessary abbreviation and redundant comment:
mesh = obj.data or me = obj.data would be more appropriate.

Getting Mesh

obj.data will give you base mesh datablock without modifiers and object deformations. In this case we need modifiers and deformations, so instead of base mesh we should get evaluated mesh and apply object transforms:

depsgraph = bpy.context.evaluated_depsgraph_get()

ob_eval = ob.evaluated_get(depsgraph)
me = ob_eval.to_mesh()
me.transform(ob.matrix_world)

# Stuff happens

ob_eval.to_mesh_clear()

If we do not apply object transforms to mesh and instead just scale object itself, then offset will also be scaled (which is what currently happens).

**Code style** Here `m = obj.data # mesh` is unnecessary abbreviation and redundant comment: `mesh = obj.data` or `me = obj.data` would be more appropriate. **Getting Mesh** `obj.data` will give you base mesh datablock without modifiers and object deformations. In this case we need modifiers and deformations, so instead of base mesh we should get evaluated mesh and apply object transforms: ``` depsgraph = bpy.context.evaluated_depsgraph_get() ob_eval = ob.evaluated_get(depsgraph) me = ob_eval.to_mesh() me.transform(ob.matrix_world) # Stuff happens ob_eval.to_mesh_clear() ``` If we do not apply object transforms to mesh and instead just scale object itself, then offset will also be scaled (which is what currently happens).
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +867,4 @@
nverts = len(m.vertices)
ntris = len(m.loop_triangles)
verts = np.zeros(3*nverts, dtype=np.float32)
tris = np.zeros(3*ntris, dtype=np.int32)

Whitespace missing around multiplication operator, same thing below with division.

Whitespace missing around multiplication operator, same thing below with division.
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +877,4 @@
half_width = max(3.0, math.ceil(abs(offset)/resolution) + 2.0) # half_width has to envelop offset
trans = vdb.Transform()
trans.scale(resolution)
levelset = vdb.FloatGrid.createLevelSetFromPolygons(verts, triangles=tris,

Trailing whitespace and hard to read. Either do a newline for each attribute or keep it single line, it's not that long anyway.

Trailing whitespace and hard to read. Either do a newline for each attribute or keep it single line, it's not that long anyway.
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +882,4 @@
# Generate offset surface
newverts, newquads = levelset.convertToQuads(offset)
polys = [x for x in newquads]

What is happening here? If you need to convert to list just use newquads = list(newquads).

What is happening here? If you need to convert to list just use `newquads = list(newquads)`.
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +885,4 @@
polys = [x for x in newquads]
# Instantiate new object in Blender
mesh = bpy.data.meshes.new(m.name + ' offset')

It would be better to name it mesh_hollow to avoid shadowing and make intention clearer.

It would be better to name it `mesh_hollow` to avoid shadowing and make intention clearer.
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +888,4 @@
mesh = bpy.data.meshes.new(m.name + ' offset')
mesh.from_pydata(newverts, [], polys)
newobj = bpy.data.objects.new(obj.name + ' offset', mesh)
newobj.matrix_world = obj.matrix_world.copy()

We do not need to copy object transforms, it already happens in evaluated mesh.

We do not need to copy object transforms, it already happens in evaluated mesh.
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +891,4 @@
newobj.matrix_world = obj.matrix_world.copy()
bpy.context.collection.objects.link(newobj)
if not join:

I really think that join feature is not good in this case, since we use evaluated mesh and original object could have a lot of modifiers, like Subd which will freeze/crash Blender if voxel density is too high.

Let's leave it to the user on what they want to do with the result.

I really think that join feature is not good in this case, since we use evaluated mesh and original object could have a lot of modifiers, like Subd which will freeze/crash Blender if voxel density is too high. Let's leave it to the user on what they want to do with the result.
Author
Contributor

Thank you for your feed back!

I very much agree with all the other points. However, on the case of join I have a counter proposal.

If we operate on the evaluated and scaled mesh, joining the generated offset surface with the target object really does not make any sense. But in this case, the offset surface on its own is not very useful for the user either. In order to generate an actually hollow object, they would have to apply modifiers and scale to (possibly a copy of) the target, flip the normals of the target or the generated surface, depending on the sign of the offset, and finally join the two. That is a somewhat complex series of operations that I think could be difficult for many users.

What if we offer the user the option to perform this operations for them? My idea is roughly this:

  1. instead of using a temporarily mesh from obj.to_mesh(), instantiate the mesh with modifiers in bpy.data.meshes
  2. use this mesh (scaled, etc.) as the basis for the offset surface
  3. if the user just wants the offset surface, remove this mesh from bpy.data.meshes, otherwise create a new object with it, adjust normals as required, and join it with the offset surface

Instead of calling this option as "Join", it could be something like "Create hollow copy" or "Offset surface only" for its inverse.

What do you think?

Thank you for your feed back! I very much agree with all the other points. However, on the case of join I have a counter proposal. If we operate on the evaluated and scaled mesh, joining the generated offset surface with the target object really does not make any sense. But in this case, the offset surface on its own is not very useful for the user either. In order to generate an actually hollow object, they would have to apply modifiers and scale to (possibly a copy of) the target, flip the normals of the target or the generated surface, depending on the sign of the offset, and finally join the two. That is a somewhat complex series of operations that I think could be difficult for many users. What if we offer the user the option to perform this operations for them? My idea is roughly this: 1. instead of using a temporarily mesh from `obj.to_mesh()`, instantiate the mesh with modifiers in `bpy.data.meshes` 1. use this mesh (scaled, etc.) as the basis for the offset surface 1. if the user just wants the offset surface, remove this mesh from `bpy.data.meshes`, otherwise create a new object with it, adjust normals as required, and join it with the offset surface Instead of calling this option as "Join", it could be something like "Create hollow copy" or "Offset surface only" for its inverse. What do you think?

Yeah, let's do Create Hollow Copy instead.

I'm not getting what you mean by instantiate in bpy.data.meshes, we still have to use obj.to_mesh() to get evaluated mesh.
I'm not aware of native mesh.join(other_mesh) method, it seems object join operator is the only one available, so we have to use bmesh for that.

depsgraph = bpy.context.evaluated_depsgraph_get()

ob_eval = ob.evaluated_get(depsgraph)
me = ob_eval.to_mesh()
me.transform(ob.matrix_world)

# Hollow stuff happens

bm = bmesh.new()
bm.from_mesh(me)
bm.from_mesh(mesh_hollow)

ob_eval.to_mesh_clear()

mesh_hollow_copy = bpy.data.meshes.new(name)
bm.to_mesh(mesh_hollow_copy)
bm.free()

Regular hollow result would not require bmesh, we use it only to join 2 meshes together.

It also would be nice to have hollow copy placed beside original object, so something like this:

ob_hollow_copy.location.x = max(ob_orig.dimensions)
Yeah, let's do `Create Hollow Copy` instead. I'm not getting what you mean by instantiate in `bpy.data.meshes`, we still have to use `obj.to_mesh()` to get evaluated mesh. I'm not aware of native `mesh.join(other_mesh)` method, it seems object join operator is the only one available, so we have to use `bmesh` for that. ```python depsgraph = bpy.context.evaluated_depsgraph_get() ob_eval = ob.evaluated_get(depsgraph) me = ob_eval.to_mesh() me.transform(ob.matrix_world) # Hollow stuff happens bm = bmesh.new() bm.from_mesh(me) bm.from_mesh(mesh_hollow) ob_eval.to_mesh_clear() mesh_hollow_copy = bpy.data.meshes.new(name) bm.to_mesh(mesh_hollow_copy) bm.free() ``` Regular hollow result would not require bmesh, we use it only to join 2 meshes together. It also would be nice to have hollow copy placed beside original object, so something like this: ```python ob_hollow_copy.location.x = max(ob_orig.dimensions) ```
Author
Contributor

I think it is better to explain with code. I implemented my idea in the commit I just pushed. It is functional, but I've left the old join parameter. I can change its name later. Here is what I did to get the evaluated mesh:

obj = context.active_object
depsgraph = context.evaluated_depsgraph_get()
mesh_target = bpy.data.meshes.new_from_object(obj.evaluated_get(depsgraph))
mesh_target.transform(obj.matrix_world)

Now mesh_target is in bpy.data.meshes. It is then used to create the offset surface. In case the user wants also the hollow object, it can be included in its own object, otherwise it is removed from bpy.data.meshes. From what I understood from obj.to_mesh(), the mesh it produces can not be used in other objects.

In this way, we can use the join operator, and bmesh and the conversions to/from mesh aren't needed. What do you think about this approach?

Some other points:

  • I've tried to address your other points in this commit, I'm not sure I succeeded.
  • I'm not very convinced about always moving away the new hollow copy. Maybe add an option for that?
  • This line mesh_target.transform(obj.matrix_world) will not only apply the scale, but will also set the mesh origin to the global origin. This is most noticeable if the target was translated somewhere before hollowing out. I don't like this effect. I would rather that the origin of the offset surface and the new hollow object stay on the same position of the target object. I'll try to find a way to do that.
I think it is better to explain with code. I implemented my idea in the commit I just pushed. It is functional, but I've left the old `join` parameter. I can change its name later. Here is what I did to get the evaluated mesh: ``` obj = context.active_object depsgraph = context.evaluated_depsgraph_get() mesh_target = bpy.data.meshes.new_from_object(obj.evaluated_get(depsgraph)) mesh_target.transform(obj.matrix_world) ``` Now `mesh_target` is in `bpy.data.meshes`. It is then used to create the offset surface. In case the user wants also the hollow object, it can be included in its own object, otherwise it is removed from `bpy.data.meshes`. From what I understood from `obj.to_mesh()`, the mesh it produces can not be used in other objects. In this way, we can use the join operator, and `bmesh` and the conversions to/from mesh aren't needed. What do you think about this approach? Some other points: - I've tried to address your other points in this commit, I'm not sure I succeeded. - I'm not very convinced about always moving away the new hollow copy. Maybe add an option for that? - This line `mesh_target.transform(obj.matrix_world)` will not only apply the scale, but will also set the mesh origin to the global origin. This is most noticeable if the target was translated somewhere before hollowing out. I don't like this effect. I would rather that the origin of the offset surface and the new hollow object stay on the same position of the target object. I'll try to find a way to do that.

What do you think about this approach?

Your approach is fine, the intent is not as clear with new_from_object, but that is semantics, functionality is exactly the same.

mesh it produces can not be used in other objects

You cannot use the result of to_mesh directly, but you can create a copy of it in bpy.data.meshes like so me.copy()

not very convinced about always moving away the new hollow copy

That is not required, keeping it in place is fine.

will not only apply the scale, but will also set the mesh origin to the global origin

You got it a bit wrong, it does not affect origin, it just transforms mesh with object transforms, all new objects have their location in the center of the scene.

I would rather that the origin of the offset surface and the new hollow object stay on the same position of the target object. I'll try to find a way to do that.

That's easy:

from mathutils import Matrix

mat = Matrix.Translation(ob_orig.matrix_world.translation)
ob_copy.matrix_world = mat
mat.invert()
ob_copy.data.transform(mat)

I've left the old join parameter

It should be called Hollow Copy or Hollow Duplicate, the original object is not changed, we're not joining it with anything.

> What do you think about this approach? Your approach is fine, the intent is not as clear with `new_from_object`, but that is semantics, functionality is exactly the same. > mesh it produces can not be used in other objects You cannot use the result of `to_mesh` directly, but you can create a copy of it in `bpy.data.meshes` like so `me.copy()` > not very convinced about always moving away the new hollow copy That is not required, keeping it in place is fine. > will not only apply the scale, but will also set the mesh origin to the global origin You got it a bit wrong, it does not affect origin, it just transforms mesh with object transforms, all new objects have their location in the center of the scene. > I would rather that the origin of the offset surface and the new hollow object stay on the same position of the target object. I'll try to find a way to do that. That's easy: ```python from mathutils import Matrix mat = Matrix.Translation(ob_orig.matrix_world.translation) ob_copy.matrix_world = mat mat.invert() ob_copy.data.transform(mat) ``` > I've left the old join parameter It should be called `Hollow Copy` or `Hollow Duplicate`, the original object is not changed, we're not joining it with anything.

hollow object stay on the same position of the target object

Or alternatively strip world matrix of translation component.

mat = ob.matrix_world.copy()
mat.translation = 0, 0, 0

me.transform(mat)

ob_hollow.matrix_world.translation = ob_orig.matrix_world.translation
> hollow object stay on the same position of the target object Or alternatively strip world matrix of translation component. ```python mat = ob.matrix_world.copy() mat.translation = 0, 0, 0 me.transform(mat) ob_hollow.matrix_world.translation = ob_orig.matrix_world.translation ```
Author
Contributor

You got it a bit wrong, it does not affect origin, it just transforms mesh with object transforms, all new objects have their location in the center of the scene

I see. Thanks for the info! Now I understand a problem I had with some of my other scripts.

Or alternatively strip world matrix of translation component.

That worked like a charm! Thanks again!

I've updated the branch with this idea. I've also changed the property name, but now I see I did not use your name suggestion, which I think is better. I'll change it later ( again 😞)

Do you have any more feedback/suggestions/comments about this part?

>You got it a bit wrong, it does not affect origin, it just transforms mesh with object transforms, all new objects have their location in the center of the scene I see. Thanks for the info! Now I understand a problem I had with some of my other scripts. > Or alternatively strip world matrix of translation component. That worked like a charm! Thanks again! I've updated the branch with this idea. I've also changed the property name, but now I see I did not use your name suggestion, which I think is better. I'll change it later ( again 😞) Do you have any more feedback/suggestions/comments about this part?

Do you have any more feedback/suggestions/comments about this part?

Tool execution is fine, now we need to tackle UI. I'll do it in a separate thread.

> Do you have any more feedback/suggestions/comments about this part? Tool execution is fine, now we need to tackle UI. I'll do it in a separate thread.
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +911,4 @@
context.view_layer.objects.active = obj
bpy.ops.object.join()
if mode_orig == 'EDIT_MESH':

That's unnecessary, just check if in edit mode in invoke method and if true switch to object mode, switching back is not needed here.

That's unnecessary, just check if in edit mode in `invoke` method and if true switch to object mode, switching back is not needed here.
MikhailRachinskiy marked this conversation as resolved
usfreitas added 1 commit 2024-02-25 15:27:21 +01:00
usfreitas added 1 commit 2024-02-26 21:45:08 +01:00
Mikhail Rachinskiy requested changes 2024-02-27 21:37:11 +01:00
Dismissed
Mikhail Rachinskiy left a comment
Member

It's a lot, but after that it all done for the exception of one issue which we'll discuss later after UI part is handled.

It's a lot, but after that it all done for the exception of one issue which we'll discuss later after UI part is handled.
@ -124,0 +138,4 @@
name = "Create Hollow Copy",
description = "Create a hollow copy of the target object, with applied modifiers and scale",
default = False
)

Duplicating operator settings in the scene is redundant, it is much better to invoke popup dialog with operator settings:

def invoke(self, context, event):
    wm = context.window_manager
    return wm.invoke_props_dialog(self)

If you want to let user choose defaults, then add-on preferences should be used instead.

Duplicating operator settings in the scene is redundant, it is much better to invoke popup dialog with operator settings: ```python def invoke(self, context, event): wm = context.window_manager return wm.invoke_props_dialog(self) ``` If you want to let user choose defaults, then add-on preferences should be used instead.
Author
Contributor

This is cool! I did not know about it. I much prefer the dialog, as the operator UI can then be just a button.

Duplication of the operator settings is a pattern that occur often in the 3D-Print Toolbox. I actually learned it by reading the add-on code. The advantage I see with it is that the last used values get saved along with the Blender file when the user saves it. This gives a set of defaults adapted to the objects. If I understand correctly, add-on preferences are global, so it is not possible to have a per file set of add-on preferences. I also like the fact the user does not need to do anything to save the defaults in the duplicated settings case.

My option would be to use the popup dialog and keep the duplicated settings in the scene. If the duplicated setting are a no go, I would just use hard coded defaults. I don't think add-on preferences are worth it here.

This is cool! I did not know about it. I much prefer the dialog, as the operator UI can then be just a button. Duplication of the operator settings is a pattern that occur often in the 3D-Print Toolbox. I actually learned it by reading the add-on code. The advantage I see with it is that the last used values get saved along with the Blender file when the user saves it. This gives a set of defaults adapted to the objects. If I understand correctly, add-on preferences are global, so it is not possible to have a per file set of add-on preferences. I also like the fact the user does not need to do anything to save the defaults in the duplicated settings case. My option would be to use the popup dialog and keep the duplicated settings in the scene. If the duplicated setting are a no go, I would just use hard coded defaults. I don't think add-on preferences are worth it here.
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +828,4 @@
bl_idname = "mesh.print3d_hollow"
bl_label = "Create offset surface"
bl_description = "Create offset surface for hollowing objects"
bl_options = {'REGISTER', 'UNDO'}
  • Operator should be called Hollow, it is short and widely accepted terminology.
  • Description should be shorter as well: Create offset surface it describes exactly what the tool does.
- Operator should be called `Hollow`, it is short and widely accepted terminology. - Description should be shorter as well: `Create offset surface` it describes exactly what the tool does.
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +847,4 @@
name = "Create Hollow Copy",
description = "Create a hollow copy of the target object, with applied modifiers and scale",
default = False
)

Code style

  • There should be no whitespace around = operator for arguments in function/object call.

Offset

  • I don't think we should use negative as default here, similar to Solidify modifier the number describes wall thickness and negative numbers just don't seem right. So, let's just flip the sign here.
  • Don't mention offset direction in description, after sign flip it will work intuitively.
  • Default offset of 5 is too much, let's do 1, it's closer to minimum thickness for 3D printed stuff.
  • Let's define float property step attribute step=1, it just makes slider drag a bit more pleasant.

Resolution

  • The name is ambiguous, let's use Voxel Size it is already used across Blender and is much more descriptive.
  • Description: just take the description of Voxel Size property from remesh modifier but remove the mention of object space.
  • Default value is too big, let's get closer to default of remesh modifier, somewhere between 0.1 and 0.5. I usually do it with 0.3 when I don't need the detail.
  • min is too big, both sculpt remesh and modifier have it at 0.0001
  • step=1

Hollow Copy

  • Boolean property name (as in variable name in code) should use certain name prefixes as a convention: use_, show_, make_, etc., whichever is more applicable in given context make_hollow_duplicate
  • Property UI name should be just Hollow Copy or Hollow Duplicate, I think duplicate is better, because there is already tool called duplicate, but copy is also fine.
  • Description: Create hollowed out copy of the object, I don't think we need to clarify scale and modifiers part, it's kind of obvious given the nature of the tool.
  • default = False default of boolean property is false, you don't need to specify it.
## Code style - There should be no whitespace around `=` operator for arguments in function/object call. ## Offset - I don't think we should use negative as default here, similar to Solidify modifier the number describes wall thickness and negative numbers just don't seem right. So, let's just flip the sign here. - Don't mention offset direction in description, after sign flip it will work intuitively. - Default offset of `5` is too much, let's do `1`, it's closer to minimum thickness for 3D printed stuff. - Let's define float property step attribute `step=1`, it just makes slider drag a bit more pleasant. ## Resolution - The name is ambiguous, let's use `Voxel Size` it is already used across Blender and is much more descriptive. - Description: just take the description of Voxel Size property from remesh modifier but remove the mention of object space. - Default value is too big, let's get closer to default of remesh modifier, somewhere between `0.1` and `0.5`. I usually do it with `0.3` when I don't need the detail. - `min` is too big, both sculpt remesh and modifier have it at `0.0001` - `step=1` ## Hollow Copy - Boolean property name (as in variable name in code) should use certain name prefixes as a convention: `use_`, `show_`, `make_`, etc., whichever is more applicable in given context `make_hollow_duplicate` - Property UI name should be just `Hollow Copy` or `Hollow Duplicate`, I think duplicate is better, because there is already tool called duplicate, but copy is also fine. - Description: `Create hollowed out copy of the object`, I don't think we need to clarify scale and modifiers part, it's kind of obvious given the nature of the tool. - `default = False` default of boolean property is false, you don't need to specify it.
Author
Contributor

Thanks again for your feedback. As for the last time, I agree with most points. I have, however, observations.

Offset

I was rather too liberal with comments in the code, but I think I still managed not to explain an important point.
There are two distinct use cases for this tool. One is the standard hollowing out, where the offset surface is on the inside of the the target object. The other use case is when the offset surface is on the outside of the target. Its application is to generate a thin walled mesh that, after printing, can be used as a mold for the target object to cast other materials in the target form. If the mold is one-shot, that is, it is discarded or destroyed after casing a single part, this technique is sometimes called eggshell molding.

While this other use case is definitely not so common as the first one, I still would like to keep it in the same operator. The application domain is the same (3d printing), and the two use cases share so much code that I think it makes sense to keep them together.

This second use case, let's call it mold making, is the reason why I used a signed offset as a parameter and mentioned the direction in the description. The "Negative -> inwards" has a counter part that means "positive -> outwards", which produces this other effect.

If we keep this mold making in the operator, there need to be some way to differentiate an inwards from an outwards offset surface. I used the sign convention of negative offset to inwards simply because that is the convention of OpenVDB when generating offset surfaces. Another possibility is to use the opposite sign convention, or to use two parameters like the Solidify modifier. Solidify uses one unsigned parameter, Thickness, for the amount of offset, and a signed parameter, Offset, to indicate inwards or outwards, with the convention of negative meaning inwards.

I'm open to the way to indicate that to the user, but I really would like to keep both functionalities in the operator, if possible.

My other point is about the default values for the voxel size. However, it is late here so I comment about that later. I'm also currently traveling on vacation, so don't be alarmed if I don't update as often as before.

Thanks again for your feedback. As for the last time, I agree with most points. I have, however, observations. ### Offset I was rather too liberal with comments in the code, but I think I still managed not to explain an important point. There are _two_ distinct use cases for this tool. One is the standard hollowing out, where the offset surface is on the inside of the the target object. The other use case is when the offset surface is on the _outside_ of the target. Its application is to generate a thin walled mesh that, after printing, can be used as a mold for the target object to cast other materials in the target form. If the mold is one-shot, that is, it is discarded or destroyed after casing a single part, this technique is sometimes called _eggshell molding_. While this other use case is definitely not so common as the first one, I still would like to keep it in the same operator. The application domain is the same (3d printing), and the two use cases share so much code that I think it makes sense to keep them together. This second use case, let's call it _mold making_, is the reason why I used a signed offset as a parameter and mentioned the direction in the description. The "Negative -> inwards" has a counter part that means "positive -> outwards", which produces this other effect. If we keep this mold making in the operator, there need to be some way to differentiate an inwards from an outwards offset surface. I used the sign convention of negative offset to inwards simply because that is the convention of OpenVDB when generating offset surfaces. Another possibility is to use the opposite sign convention, or to use two parameters like the Solidify modifier. Solidify uses one unsigned parameter, _Thickness_, for the amount of offset, and a signed parameter, _Offset_, to indicate inwards or outwards, with the convention of negative meaning inwards. I'm open to the way to indicate that to the user, but I really would like to keep both functionalities in the operator, if possible. My other point is about the default values for the voxel size. However, it is late here so I comment about that later. I'm also currently traveling on vacation, so don't be alarmed if I don't update as often as before.

I'm not suggesting removing the bi-directional offset functionality, just flip the sign, so positive would mean inward and negative outward, similar to solidify modifier.

The only software that I know of that does negative inward is MeshLab, the rest of them have a separate direction switch toggle.

Have a nice time traveling!

I'm not suggesting removing the bi-directional offset functionality, just flip the sign, so positive would mean inward and negative outward, similar to solidify modifier. The only software that I know of that does negative inward is MeshLab, the rest of them have a separate direction switch toggle. Have a nice time traveling!
Author
Contributor

I'm back.

Regarding offset sign, I would rather go with an unsigned thickness parameter and a direction switch toggle, instead of just flipping the sign. I think it would be easier for users to understand.

Regarding the voxel size (this name is really better), I think we should be ware of setting the default too low. I see two reasons for that.

The first is that, when using Blender for 3D printing, the size of the objects in units tend to be larger compared to the default 0.1 of the remesh modifier. At least in my case, where I have my units in millimeters, and my objects easily reach the hundreds. I usually freeze or crash Blender if I'm not careful when adding a remesh modifier, for instance. In our case, if the voxel size is too small compared with the object, the level set creation in OpenVDB can use an excessive amount of memory. Another potential problem is when the voxel size is small compared with the offset. Since the half width of the level set needs to envelop the offset, this can lead to a very large number of active voxels in the level set, again needing a large amount of memory.

The other reason that I see is that a highly detailed offset surface is neither needed nor generally desired for hollowing out objects for printing, IMO. I personally prefer the offset surface to be low poly compared to the target object.

I ran some tests with the Stanford bunny in millimeters. The model is about 150mm long. I've generated an outside surface with a 2mm offset. Using a voxel size of 0.5mm it took a couple of seconds to execute and generated a mesh with more than ten times the number of triangles of the original model. With a voxel size of 0.3, it took 8 seconds to run and produced about 30x more tris than the model. A voxel size of 0.1 crashed my Blender. With 2mm voxel size, it runs instantly and the result has about 25% less tris than the model. For mold making, I think this latter result is still oversampled and could be made with even less tris.

With this in mind, I would suggest using a default of around 1.0 or larger, as a smaller offset could cause the problems above.

Another way of dealing with this question would be to have dynamic defaults. If we could somehow compute a voxel size adapted to the target object and offset value and set the default to this value, this would be a better solution than a hardcoded default. But I have no idea how to do this, or if dynamic defaults are even possible.

I'm back. Regarding offset sign, I would rather go with an unsigned thickness parameter and a direction switch toggle, instead of just flipping the sign. I think it would be easier for users to understand. Regarding the voxel size (this name is really better), I think we should be ware of setting the default too low. I see two reasons for that. The first is that, when using Blender for 3D printing, the size of the objects in units tend to be larger compared to the default 0.1 of the remesh modifier. At least in my case, where I have my units in millimeters, and my objects easily reach the hundreds. I usually freeze or crash Blender if I'm not careful when adding a remesh modifier, for instance. In our case, if the voxel size is too small compared with the object, the level set creation in OpenVDB can use an excessive amount of memory. Another potential problem is when the voxel size is small compared with the offset. Since the half width of the level set needs to envelop the offset, this can lead to a very large number of active voxels in the level set, again needing a large amount of memory. The other reason that I see is that a highly detailed offset surface is neither needed nor generally desired for hollowing out objects for printing, IMO. I personally prefer the offset surface to be low poly compared to the target object. I ran some tests with the Stanford bunny in millimeters. The model is about 150mm long. I've generated an outside surface with a 2mm offset. Using a voxel size of 0.5mm it took a couple of seconds to execute and generated a mesh with more than ten times the number of triangles of the original model. With a voxel size of 0.3, it took 8 seconds to run and produced about 30x more tris than the model. A voxel size of 0.1 crashed my Blender. With 2mm voxel size, it runs instantly and the result has about 25% less tris than the model. For mold making, I think this latter result is still oversampled and could be made with even less tris. With this in mind, I would suggest using a default of around 1.0 or larger, as a smaller offset could cause the problems above. Another way of dealing with this question would be to have dynamic defaults. If we could somehow compute a voxel size adapted to the target object and offset value and set the default to this value, this would be a better solution than a hardcoded default. But I have no idea how to do this, or if dynamic defaults are even possible.
Author
Contributor

I've committed some code. I've tried to make all the suggested changes, with some exceptions:

  • Default voxel size is 1.0
  • Offset now is only positive. Direction is indicated by a new boolean property.
  • Operator properties are still duplicated in the scene. The scene properties are updated when the operator concludes.

I've also changed the sequence of operations a bit, and now it can happen that the offset surface have its normals flipped twice. However, I think it is worth it due to the simpler logic and better code legibility.

I would like to thank you again for the time you are committing to this code review. I've learned a lot, and using the tool feels much better. I think code quality also improved significantly.

I've committed some code. I've tried to make all the suggested changes, with some exceptions: - Default voxel size is 1.0 - Offset now is only positive. Direction is indicated by a new boolean property. - Operator properties are still duplicated in the scene. The scene properties are updated when the operator concludes. I've also changed the sequence of operations a bit, and now it can happen that the offset surface have its normals flipped twice. However, I think it is worth it due to the simpler logic and better code legibility. I would like to thank you again for the time you are committing to this code review. I've learned a lot, and using the tool feels much better. I think code quality also improved significantly.
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +892,4 @@
obj_offset = bpy.data.objects.new(obj.name + ' offset', mesh_offset)
bpy.context.collection.objects.link(obj_offset)
if not self.create_hollow:

It's better to avoid negation, if possible, it just makes harder to follow the logic.

if self.create_hollow:
    ...
else:
    ...
It's better to avoid negation, if possible, it just makes harder to follow the logic. ```python if self.create_hollow: ... else: ... ```
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +898,4 @@
# Translate surface object to target object's position
obj_offset.matrix_world.translation = obj.matrix_world.translation
# This mesh will not be used anymore
bpy.data.meshes.remove(mesh_target)

These comments are redundant, it's obvious why you flip normals, why you move new object to a location of current object, or why you remove mesh (you could name it mesh_temp to emphasize intent).

These comments are redundant, it's obvious why you flip normals, why you move new object to a location of current object, or why you remove mesh (you could name it `mesh_temp` to emphasize intent).
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +903,4 @@
# Create a copy of the target object with applied modifiers and scale
obj_hollow = bpy.data.objects.new(obj.name + " hollow", mesh_target)
bpy.context.collection.objects.link(obj_hollow)
if self.offset < 0.0:

This code block is useless, just do if self.offset > 0.0

This code block is useless, just do `if self.offset > 0.0`
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +910,4 @@
# Offset surface is outside, correct normals, see above
mesh_offset.flip_normals()
# Original surface is inside, flip its normals
mesh_target.flip_normals()

Again, you don't need to comment on every line, there is a check above if create_hollow and you are using bpy.data.objects.new right after it, the code is self-explanatory.

Same with offset check.

Again, you don't need to comment on every line, there is a check above `if create_hollow` and you are using `bpy.data.objects.new` right after it, the code is self-explanatory. Same with offset check.
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +917,4 @@
obj_offset.select_set(True)
obj_hollow.select_set(True)
context.view_layer.objects.active = obj_hollow
bpy.ops.object.join()

Part of this code should be outside if block, currently selection changes only when hollow duplicate is created. It should change regardless of that, new object is added to the scene - it should be selected and active.

Part of this code should be outside `if` block, currently selection changes only when hollow duplicate is created. It should change regardless of that, new object is added to the scene - it should be selected and active.
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +920,4 @@
bpy.ops.object.join()
# Translate hollow object to target object's position
obj_hollow.matrix_world.translation = obj.matrix_world.translation

If you find matrix stuff confusing, then you could write it as:

obj_hollow.location = obj.matrix_world.translation

It actually could be like this, but there is a chance that object could have a parent, so using matrix_world ensures we have visible location.

obj_hollow.location = obj.location 
If you find matrix stuff confusing, then you could write it as: ```python obj_hollow.location = obj.matrix_world.translation ``` It actually could be like this, but there is a chance that object could have a parent, so using matrix_world ensures we have visible location. ```python obj_hollow.location = obj.location ```
Author
Contributor

I'm actually pretty comfortable around matrices. I had a good training in linear algebra, and I've been using NumPy way longer than Blender.

I was looking for something like obj_hollow.matrix_world[:3, 3] = ... and had not realized that mathutils.Matrix.translation is writable until you suggested it. That code line, as it is now, makes perfect sense for me, and I would not have added the comment above it if I were the only one touching this add-on.

However, I have very little experience in programing with other people. As I was afraid that what I found easy to understand might not be that for the next one who needs to work in this code, I went trigger happy with the comments 😅

I'll remove this and the other redundant comments.

I'm actually pretty comfortable around matrices. I had a good training in linear algebra, and I've been using NumPy way longer than Blender. I was looking for something like `obj_hollow.matrix_world[:3, 3] = ...` and had not realized that `mathutils.Matrix.translation` is writable until you suggested it. That code line, as it is now, makes perfect sense for me, and I would not have added the comment above it if I were the only one touching this add-on. However, I have very little experience in programing with other people. As I was afraid that what I found easy to understand might not be that for the next one who needs to work in this code, I went trigger happy with the comments 😅 I'll remove this and the other redundant comments.
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +922,4 @@
# Translate hollow object to target object's position
obj_hollow.matrix_world.translation = obj.matrix_world.translation

Double newline only allowed in-between class and function definitions, everything inside function body should be separated by 1 line max.

Double newline only allowed in-between class and function definitions, everything inside function body should be separated by 1 line max.
MikhailRachinskiy marked this conversation as resolved
@ -121,0 +123,4 @@
col.prop(print_3d, "hollow_create")
col.prop(print_3d, "hollow_offset")
col.prop(print_3d, "hollow_resolution")
col.operator("mesh.print3d_hollow", text="Create Offset Surface")

The name of the panel should be changed from Transform to Edit, Hollow tool should be placed first (no additional label needed).

The name of the panel should be changed from `Transform` to `Edit`, Hollow tool should be placed first (no additional label needed).
MikhailRachinskiy marked this conversation as resolved
usfreitas added 2 commits 2024-03-13 17:27:48 +01:00
Mikhail Rachinskiy requested changes 2024-03-14 08:02:38 +01:00
Dismissed
Mikhail Rachinskiy left a comment
Member

Nice to have you back, meanwhile I got the flu and currently coughing my lungs out, but it's getting better.

The work is mostly done, just a couple of things left, and then it's merge ready.

Nice to have you back, meanwhile I got the flu and currently coughing my lungs out, but it's getting better. The work is mostly done, just a couple of things left, and then it's merge ready.
@ -821,0 +841,4 @@
voxel_size: FloatProperty(
name="Voxel size",
description="Size of the voxel used for volume evaluation. Lower values preserve finer details",
default=5.0,

default=1

default=1
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +854,4 @@
name="Inside",
description="Offset surface inside of the object",
default=True,
)

I think that simple checkbox called inside is not user friendly, it is fine with on/off options, but with something more complex it's better to make all available options visible right away (even when there is only two options).

Expanded enum called Offset Direction with values Inside and Outside fits better here.

Also, it is better to group related options together, so place Offset Direction right before Offset option.

I think that simple checkbox called inside is not user friendly, it is fine with on/off options, but with something more complex it's better to make all available options visible right away (even when there is only two options). Expanded enum called `Offset Direction` with values `Inside` and `Outside` fits better here. Also, it is better to group related options together, so place `Offset Direction` right before `Offset` option.

You did not expand enum property so it currently shows as dropdown list.

Add draw method to the operator, right before execute method and manually compose UI:

def draw(self, context):
    layout = self.layout
    layout.use_property_split = True
    layout.use_property_decorate = False

    layout.prop(self, "offset_direction", expand=True)
    layout.prop(self, "offset")
    layout.prop(self, "voxel_size")
    layout.prop(self, "make_hollow_duplicate")

If you want to keep presets, then throw layout.separator() in there, to keep it visually separated:

def draw(self, context):
    ...
    layout.use_property_decorate = False

    layout.separator()

    layout.prop(self, "offset_direction", expand=True)
    ...
You did not expand enum property so it currently shows as dropdown list. Add draw method to the operator, right before `execute` method and manually compose UI: ```python def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False layout.prop(self, "offset_direction", expand=True) layout.prop(self, "offset") layout.prop(self, "voxel_size") layout.prop(self, "make_hollow_duplicate") ``` If you want to keep presets, then throw `layout.separator()` in there, to keep it visually separated: ```python def draw(self, context): ... layout.use_property_decorate = False layout.separator() layout.prop(self, "offset_direction", expand=True) ... ```
Author
Contributor

I did not understand what you meant by "expand". Now I know, and it looks really better. The separator also helps.

I did not understand what you meant by "expand". Now I know, and it looks _really_ better. The separator also helps.
MikhailRachinskiy marked this conversation as resolved
@ -821,0 +929,4 @@
print_3d.hollow_offset = self.offset
print_3d.hollow_voxel_size = self.voxel_size
print_3d.make_hollow_duplicate = self.make_hollow_duplicate
print_3d.make_hollow_inside = self.make_hollow_inside

The scene settings have to go, it's hidden behavior and is really convoluted without clear benefit, and for reusing settings it is better to use presets.
Just add PRESET value to bl_options.

bl_options = {'REGISTER', 'UNDO', 'PRESET'}
The scene settings have to go, it's hidden behavior and is really convoluted without clear benefit, and for reusing settings it is better to use presets. Just add `PRESET` value to `bl_options`. ```python bl_options = {'REGISTER', 'UNDO', 'PRESET'} ```
Author
Contributor

I'm sorry about you being sick. I hope when you will be already fully recovered when you read this.

This time I implemented all suggestions. At least I hope. Last time a wrong default flew by... 😅

I'm not very happy about the preset. I think it cluttered a bit an interface that was otherwise very neat. But I think it is better to let that in and give the user the option to customize the operator.

Thanks again! This is fun :D

I'm sorry about you being sick. I hope when you will be already fully recovered when you read this. This time I implemented all suggestions. At least I hope. Last time a wrong default flew by... 😅 I'm not very happy about the preset. I think it cluttered a bit an interface that was otherwise very neat. But I think it is better to let that in and give the user the option to customize the operator. Thanks again! This is fun :D

Adding presets was just a suggestion, the operator settings are quite minimal so presets not necessary, you can remove it if you want.

Adding presets was just a suggestion, the operator settings are quite minimal so presets not necessary, you can remove it if you want.
Author
Contributor

With the separator it looks definitely better.

I think the preset is useful because of my points about the default voxel size. If a user has a setup where this default isn't appropriate, having the possibility to customize it is a plus. I wish I could do that with the remesh modifier.

With the separator it looks definitely better. I think the preset is useful because of my points about the default voxel size. If a user has a setup where this default isn't appropriate, having the possibility to customize it is a plus. I wish I could do that with the remesh modifier.
MikhailRachinskiy marked this conversation as resolved
usfreitas added 1 commit 2024-03-16 18:18:38 +01:00
usfreitas added 1 commit 2024-03-16 19:03:22 +01:00
Mikhail Rachinskiy reviewed 2024-03-17 06:07:02 +01:00
@ -821,0 +839,4 @@
name="Offset Direction",
description="Where the offset surface is created relative to the object",
default='inside',
)

Enum values are supposed to be in UPPERCASE


I noticed inconsistent use of double vs single quotation marks, Blender code guidelines recommends: single quotes when using enum values, double quotes everything else.

Enum values are supposed to be in `UPPERCASE` --- I noticed inconsistent use of double vs single quotation marks, Blender code guidelines recommends: single quotes when using enum values, double quotes everything else.
Author
Contributor

Sorry, my bad. It should be fixed now.

Sorry, my bad. It should be fixed now.
MikhailRachinskiy marked this conversation as resolved
usfreitas added 1 commit 2024-03-17 07:59:19 +01:00

OK it's ready for merge. There is just one more thing that I wanted to discuss with you.

I noticed there are two reasons hollow gives empty result (object with no mesh):

  1. Offset value is more than half of the object thickness.
  2. Mesh has a big hole in it.

Would be nice to have some warning, but the problem is – we cannot definitively know why result is empty.
So, warning would be ambiguous: Make sure target mesh has closed surface and offset value is not greater than max thickness.

What's your thoughts on that?

OK it's ready for merge. There is just one more thing that I wanted to discuss with you. I noticed there are two reasons hollow gives empty result (object with no mesh): 1. Offset value is more than half of the object thickness. 2. Mesh has a big hole in it. Would be nice to have some warning, but the problem is – we cannot definitively know why result is empty. So, warning would be ambiguous: `Make sure target mesh has closed surface and offset value is not greater than max thickness.` What's your thoughts on that?
Author
Contributor

Good catch. I've mostly ignored failure conditions.

The first reason is due to too large an offset. The other reason is when the mesh does not define a closed space. I think those conditions can only cause problems for internal offset surfaces. External offsets seam to be always possible.

We may be able to differentiate between these conditions. If the mesh is closed, there will be values below zero in the level set. The level set defines a narrow grid close to the mesh and encodes a signed distance field, with negative values meaning inside.The most negative value of this grid should represent the maximum offset possible for inside surfaces.

If the mesh is not closed, OpenVDB will not be able to determine the inside of it, and all values on the grid will be non negative.

That is how I understand it. However, when I tried to get those minimum and maximum values, I still got negative values for open meshes, and I don' t know why. I used the evalMinMax() function to get the extrema. If we can get this values reliably, and they are consistent with I understood of how OpenVDB works, we may give a more clear error message.

I'll dive into OpenVDB for a while to see if I can fish out something.

Good catch. I've mostly ignored failure conditions. The first reason is due to too large an offset. The other reason is when the mesh does not define a closed space. I think those conditions can only cause problems for internal offset surfaces. External offsets seam to be always possible. We may be able to differentiate between these conditions. If the mesh is closed, there will be values below zero in the level set. The level set defines a narrow grid close to the mesh and encodes a signed distance field, with negative values meaning inside.The most negative value of this grid should represent the maximum offset possible for inside surfaces. If the mesh is not closed, OpenVDB will not be able to determine the inside of it, and all values on the grid will be non negative. That is how I understand it. However, when I tried to get those minimum and maximum values, I still got negative values for open meshes, and I don' t know why. I used the `evalMinMax()` function to get the extrema. If we can get this values reliably, and they are consistent with I understood of how OpenVDB works, we may give a more clear error message. I'll dive into OpenVDB for a while to see if I can fish out something.
Author
Contributor

Well, I could not find something very informative in the OpenVDB docs. The only thing I could find is that a closed surface is needed.

So I went for some tests. I added print(levelset.info(5)) after the level set creation (not sure about 5 being required).

For open surfaces, the minimum value is small in magnitude. It is slightly positive or close to zero for simple surfaces, but it can be around -voxel_size for complex objects. I tried with Suzanne without the eyes. I don't know how negative this value can go, but I suspect it will never be much grater than the voxel size in magnitude for open surfaces.

For closed surfaces, the minimum value behaves as I expect. For instance, for an sphere of radius 15, it is a little greater than -15.

Therefore I think we could use this as a way to provide a better error message. Here is suggestion for error reporting logic:

Got empty mesh from OpenVDB:

  • If OUTSIDE:
    • Weird error. Should not happen. Report general malfunction.
  • If INSIDE:
    • Get levelset min value.
    • If min value magnitude less then 2 * voxel size:
      • Report: "Open surface or target too thin or voxel size too big"
    • Otherwise report: "Target is too thin for this offset. Offset should be less than [min levelset value]"

What do you think? And how best to report errors to the user? There should be something better than printing on the console, which is what I usually do 😅

Well, I could not find something very informative in the OpenVDB docs. The only thing I could find is that a closed surface is needed. So I went for some tests. I added `print(levelset.info(5))` after the level set creation (not sure about 5 being required). For open surfaces, the minimum value is small in magnitude. It is slightly positive or close to zero for simple surfaces, but it can be around `-voxel_size` for complex objects. I tried with Suzanne without the eyes. I don't know how negative this value can go, but I suspect it will never be much grater than the voxel size in magnitude for open surfaces. For closed surfaces, the minimum value behaves as I expect. For instance, for an sphere of radius 15, it is a little greater than -15. Therefore I think we could use this as a way to provide a better error message. Here is suggestion for error reporting logic: Got empty mesh from OpenVDB: - If `OUTSIDE`: - Weird error. Should not happen. Report general malfunction. - If `INSIDE`: - Get levelset min value. - If min value magnitude less then 2 * voxel size: - Report: "Open surface or target too thin or voxel size too big" - Otherwise report: "Target is too thin for this offset. Offset should be less than [min levelset value]" What do you think? And how best to report errors to the user? There should be something better than printing on the console, which is what I usually do 😅

In my testing I found that this: If min value magnitude less then 2 * voxel size was not a reliable way to test.

  • Suzanne model unmodified
  • Inside offset 0.1
  • Voxel size 0.3

I successfully got resulted mesh with -0.5991 min value, which is less than 2 * 0.3.
And for voxel size 0.4, min value was at -0.5371.

Other test Suzanne with no eyes, both offset and voxel size 0.1, resulted in failure (mesh was so small it was practically invisible) min value was -0.1.
So even check for newquads array will not give us 100% success rate.

Unless there is a definitive enough way to tell apart offset too big and closed mesh cases, I think we should just stick to ambiguous warning. Otherwise, we risk misinforming the user.

For reporting we'll use report method, error code makes sure that popup window will appear.

if self.offset_direction == 'INSIDE':
    newverts, newquads = levelset.convertToQuads(-self.offset)
    if newquads.size == 0:
        self.report({'ERROR'}, "Make sure target mesh has closed surface and offset value is less than half of target thickness")
        return {'FINISHED'}
else:
    ...

We'll skip check for outside direction as I couldn't make it fail, as much as I tried.

In my testing I found that this: `If min value magnitude less then 2 * voxel size` was not a reliable way to test. - Suzanne model unmodified - Inside offset 0.1 - Voxel size 0.3 I successfully got resulted mesh with -0.5991 min value, which is less than 2 * 0.3. And for voxel size 0.4, min value was at -0.5371. Other test Suzanne with no eyes, both offset and voxel size 0.1, resulted in failure (mesh was so small it was practically invisible) min value was -0.1. So even check for `newquads` array will not give us 100% success rate. Unless there is a definitive enough way to tell apart `offset too big` and `closed mesh` cases, I think we should just stick to ambiguous warning. Otherwise, we risk misinforming the user. For reporting we'll use `report` method, error code makes sure that popup window will appear. ```python if self.offset_direction == 'INSIDE': newverts, newquads = levelset.convertToQuads(-self.offset) if newquads.size == 0: self.report({'ERROR'}, "Make sure target mesh has closed surface and offset value is less than half of target thickness") return {'FINISHED'} else: ... ``` We'll skip check for outside direction as I couldn't make it fail, as much as I tried.
usfreitas added 1 commit 2024-03-18 11:26:35 +01:00
Author
Contributor

Unless there is a definitive enough way to tell apart offset too big and closed mesh cases, I think we should just stick to ambiguous warning. Otherwise, we risk misinforming the user.

Agreed. I'll dig a little more in OpenVDB, mainly because I want a method to identify when a mesh has a hole, but for now, I agree that the ambiguous error is better.

We'll skip check for outside direction as I couldn't make it fail, as much as I tried.

I don't see how it could fail. If we find a failure case later, we can always add a check for that.

It just occurred to me that an outside offset surface could be a nice alternative for the solidify modifier in some cases. I can't wait for the grids to land in Geometry Nodes :D

> Unless there is a definitive enough way to tell apart `offset too big` and `closed mesh` cases, I think we should just stick to ambiguous warning. Otherwise, we risk misinforming the user. Agreed. I'll dig a little more in OpenVDB, mainly because I want a method to identify when a mesh has a hole, but for now, I agree that the ambiguous error is better. >We'll skip check for outside direction as I couldn't make it fail, as much as I tried. I don't see how it could fail. If we find a failure case later, we can always add a check for that. It just occurred to me that an outside offset surface could be a nice alternative for the solidify modifier in some cases. I can't wait for the grids to land in Geometry Nodes :D
Mikhail Rachinskiy approved these changes 2024-03-18 12:09:51 +01:00
Mikhail Rachinskiy left a comment
Member

OK then, it is time for merge.

OK then, it is time for merge.
Mikhail Rachinskiy changed title from WIP: 3D Print Toolbox: Add hollow out to 3D Print Toolbox: Add hollow out 2024-03-18 12:17:02 +01:00
Mikhail Rachinskiy merged commit f5205cf91a into main 2024-03-18 12:24:30 +01:00
usfreitas deleted branch hollow 2024-03-18 12:59:17 +01:00
Sign in to join this conversation.
No reviewers
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: blender/blender-addons#105194
No description provided.