Add feature #12: Scale geometry options for shape import #16

Merged
Cedric Steiert merged 3 commits from Hombre57/io_scene_x3d:import-scale into main 2024-09-10 00:49:23 +02:00
2 changed files with 44 additions and 41 deletions
Showing only changes of commit 82b09447a0 - Show all commits

View File

@ -39,12 +39,12 @@ class ImportX3D(bpy.types.Operator, ImportHelper):
file_unit: EnumProperty(
name="File Unit",
items=(('M', "Meter", "The VRML/x3D specification states that data have to be exported in Meter."),
items=(('M', "Meter", "Meter is the default unit of vrml (wrl) files. Most programs assume the objects to be defined in meters as per specification"),
Bujus_Krachus marked this conversation as resolved Outdated

three improvement suggestions:

  1. No dot at the end of the sentence. A dot will be added automatically by blender (thus currently resulting in "..").
  2. Make the description more user-friendly, sth like "Meter: the default unit of vrml (wrl) files. Most programs assume the objects to be defined in meters as per specification."
  3. include scale factor (in case users prefer to set it manually)
three improvement suggestions: 1. No dot at the end of the sentence. A dot will be added automatically by blender (thus currently resulting in ".."). 2. Make the description more user-friendly, sth like "Meter: the default unit of vrml (wrl) files. Most programs assume the objects to be defined in meters as per specification." 4. include scale factor (in case users prefer to set it manually)
  1. & 2. Done
  2. no need to include the scale in tooltip, see below
1. & 2. Done 3. no need to include the scale in tooltip, see below
('DM', "Decimeter", ""),
Bujus_Krachus marked this conversation as resolved
Review

Here are the descriptions missing. Two points that could be included:

  • what programs use that unit scale
  • include the scale factor
Here are the descriptions missing. Two points that could be included: - what programs use that unit scale - include the scale factor
Review

I don't know if any software will use that scale, I don't think anyone will, but I added in the list all units between meter and millimeter. I hesitated to add the intermediate units... If you want a specific list of entries, just drop them in a comment below.

I'll add the scale in the comments for each entries.

I don't know if any software will use that scale, I don't think anyone will, but I added in the list all units between meter and millimeter. I hesitated to add the intermediate units... If you want a specific list of entries, just drop them in a comment below. I'll add the scale in the comments for each entries.
('CM', "Centimeter", ""),
Hombre57 marked this conversation as resolved
Review

description, see above

description, see above
('MM', "Milimeter", ""),
Hombre57 marked this conversation as resolved
Review

description, see above

description, see above
('IN', "Inch", ""),
Hombre57 marked this conversation as resolved
Review

description, see above

description, see above
('CUSTOM', "Use scale", "Prefer scaling the geometry with the scale factor below"),
('CUSTOM', "CUSTOM", "Use the scale factor provided below"),
Hombre57 marked this conversation as resolved Outdated
  • Instead of "Use scale" use "CUSTOM"
  • Instead of "Prefer scaling the geometry with the scale factor below" maybe rewrite to "Use the scale factor provided below"
- Instead of "Use scale" use "CUSTOM" - Instead of "Prefer scaling the geometry with the scale factor below" maybe rewrite to "Use the scale factor provided below"

Done

Done
),
description="Unit used in the input file",
default='M',
@ -54,6 +54,8 @@ class ImportX3D(bpy.types.Operator, ImportHelper):
name="Scale",
min=0.001, max=1000.0,
default=1.0,
Hombre57 marked this conversation as resolved
Review

please also add step=1.0,. Currently it steps in intervals of 0.03, 0.01 seems more appropriate (or is 0.03 more comfy?). Could be also adjusted for the x3d exporter in line 220.

please also add `step=1.0,`. Currently it steps in intervals of 0.03, 0.01 seems more appropriate (or is 0.03 more comfy?). Could be also adjusted for the x3d exporter in line 220.
Review

Done

Done
precision=4,
step=1.0,
description="Scale value used when 'File Unit' is set to 'Use scale'",
Bujus_Krachus marked this conversation as resolved Outdated

'Use scale' now needs to be adjusted to the new name (CUSTOM)

'Use scale' now needs to be adjusted to the new name (CUSTOM)
)
@ -62,6 +64,7 @@ class ImportX3D(bpy.types.Operator, ImportHelper):
keywords = self.as_keywords(ignore=("axis_forward",
"axis_up",
"file_unit",
"filter_glob",
))
global_matrix = axis_conversion(from_forward=self.axis_forward,
@ -269,7 +272,14 @@ class X3D_PT_import_transform(bpy.types.Panel):
operator = sfile.active_operator
Bujus_Krachus marked this conversation as resolved Outdated

I would prefer the scale input to be disabled until entry CUSTOM is selected (to avoid confusion for users in a rush), this could maybe be achieved by this solution: https://blender.stackexchange.com/a/268840

Also on another note a small research question: is it possible to dynamically change the value of the scale field depending on the selected dropdown entry? Idk if this would be helpful for some to understand what each scale option means or if it just clutters the importer.

I would prefer the scale input to be disabled until entry CUSTOM is selected (to avoid confusion for users in a rush), this could maybe be achieved by this solution: https://blender.stackexchange.com/a/268840 Also on another note a small research question: is it possible to dynamically change the value of the scale field depending on the selected dropdown entry? Idk if this would be helpful for some to understand what each scale option means or if it just clutters the importer.

Widget sensitivity and update done. no need for scale in tooltips then.

Widget sensitivity and update done. no need for scale in tooltips then.
layout.prop(operator, "file_unit")
layout.prop(operator, "global_scale")
sub = layout.row()
sub.enabled = operator.file_unit == 'CUSTOM'
sub.prop(operator, "global_scale")
UNITS_FACTOR = {'M': 1.0, 'DM': 0.1, 'CM': 0.01, 'MM': 0.001, 'IN': 0.0254}
if operator.file_unit != 'CUSTOM':
operator.global_scale = UNITS_FACTOR[operator.file_unit]
layout.prop(operator, "axis_forward")
layout.prop(operator, "axis_up")

View File

@ -15,7 +15,7 @@ from itertools import chain
texture_cache = {}
material_cache = {}
conversionScale = 1.0
conversion_scale = 1.0
Bujus_Krachus marked this conversation as resolved Outdated

use snake_case

use snake_case
EPSILON = 0.0000001 # Very crude.
@ -682,7 +682,7 @@ class vrmlNode(object):
print('\tvalue "%s" could not be used as an int for field "%s"' % (f[0], field))
return default
def getFieldAsFloat(self, field, default, ancestry, s=1.0):
def getFieldAsFloat(self, field, default, ancestry, scale_factor=1.0):
Bujus_Krachus marked this conversation as resolved Outdated

nitpick - be more descriptive: what does "s" mean? I sit speed, symmetry, similarity, simplicity, maybe even scale?

nitpick - be more descriptive: what does "s" mean? I sit speed, symmetry, similarity, simplicity, maybe even scale?
self_real = self.getRealNode() # in case we're an instance
f = self_real.getFieldName(field, ancestry)
@ -696,12 +696,12 @@ class vrmlNode(object):
return default
try:
return float(f[0])*s
return float(f[0])*scale_factor
except:
print('\tvalue "%s" could not be used as a float for field "%s"' % (f[0], field))
return default
def getFieldAsFloatTuple(self, field, default, ancestry, s=1.0):
def getFieldAsFloatTuple(self, field, default, ancestry, scale_factor=1.0):
Bujus_Krachus marked this conversation as resolved Outdated

nitpick - be more descriptive: what does "s" mean? I sit speed, symmetry, similarity, simplicity, maybe even scale?

nitpick - be more descriptive: what does "s" mean? I sit speed, symmetry, similarity, simplicity, maybe even scale?
self_real = self.getRealNode() # in case we're an instance
f = self_real.getFieldName(field, ancestry)
@ -717,7 +717,7 @@ class vrmlNode(object):
for v in f:
if v != ',':
try:
ret.append(float(v.strip('"'))*s)
ret.append(float(v.strip('"'))*scale_factor)
except:
break # quit of first non float, perhaps its a new field name on the same line? - if so we are going to ignore it :/ TODO
# print(ret)
@ -775,7 +775,7 @@ class vrmlNode(object):
print('\tvalue "%s" could not be used as a string for field "%s"' % (f[0], field))
return default
def getFieldAsArray(self, field, group, ancestry, s=1.0):
def getFieldAsArray(self, field, group, ancestry, scale_factor=1.0):
"""
For this parser arrays are children
"""
@ -815,7 +815,7 @@ class vrmlNode(object):
# print(child_array)
# Normal vrml
array_data = child_array.array_data
apply_scale = s != 1.0
apply_scale = scale_factor != 1.0
# print('array_data', array_data)
if group == -1 or len(array_data) == 0:
@ -834,7 +834,7 @@ class vrmlNode(object):
if apply_scale:
# applying scale
for item in array_data:
item *= s
item *= scale_factor
else:
flat_array = []
@ -846,7 +846,7 @@ class vrmlNode(object):
else:
if apply_scale:
# applying scale
item *= s
item *= scale_factor
flat_array.append(item)
@ -1509,11 +1509,11 @@ def translateScale(sca):
def translateTransform(node, ancestry):
cent = node.getFieldAsFloatTuple('center', None, ancestry, conversionScale) # (0.0, 0.0, 0.0)
cent = node.getFieldAsFloatTuple('center', None, ancestry, conversion_scale) # (0.0, 0.0, 0.0)
rot = node.getFieldAsFloatTuple('rotation', None, ancestry) # (0.0, 0.0, 1.0, 0.0)
sca = node.getFieldAsFloatTuple('scale', None, ancestry, conversionScale) # (1.0, 1.0, 1.0)
sca = node.getFieldAsFloatTuple('scale', None, ancestry, conversion_scale) # (1.0, 1.0, 1.0)
scaori = node.getFieldAsFloatTuple('scaleOrientation', None, ancestry) # (0.0, 0.0, 1.0, 0.0)
tx = node.getFieldAsFloatTuple('translation', None, ancestry, conversionScale) # (0.0, 0.0, 0.0)
tx = node.getFieldAsFloatTuple('translation', None, ancestry, conversion_scale) # (0.0, 0.0, 0.0)
if cent:
cent_mat = Matrix.Translation(cent)
@ -1553,10 +1553,10 @@ def translateTransform(node, ancestry):
def translateTexTransform(node, ancestry):
cent = node.getFieldAsFloatTuple('center', None, ancestry, conversionScale) # (0.0, 0.0)
cent = node.getFieldAsFloatTuple('center', None, ancestry, conversion_scale) # (0.0, 0.0)
rot = node.getFieldAsFloat('rotation', None, ancestry) # 0.0
sca = node.getFieldAsFloatTuple('scale', None, ancestry) # (1.0, 1.0)
tx = node.getFieldAsFloatTuple('translation', None, ancestry, conversionScale) # (0.0, 0.0)
tx = node.getFieldAsFloatTuple('translation', None, ancestry, conversion_scale) # (0.0, 0.0)
if cent:
# cent is at a corner by default
@ -1930,7 +1930,7 @@ def importMesh_IndexedFaceSet(geom, ancestry):
# TODO: resolve that somehow, so that vertex set can be effectively
# reused between different mesh types?
else:
points = coord.getFieldAsArray('point', 3, ancestry, conversionScale)
points = coord.getFieldAsArray('point', 3, ancestry, conversion_scale)
if coord.canHaveReferences():
coord.parsed = points
index = geom.getFieldAsArray('coordIndex', 0, ancestry)
@ -2441,7 +2441,7 @@ def importMesh_LineSet(geom, ancestry):
# TODO: line display properties are ignored
# Per-vertex color is ignored
coord = geom.getChildBySpec('Coordinate')
src_points = coord.getFieldAsArray('point', 3, ancestry, conversionScale)
src_points = coord.getFieldAsArray('point', 3, ancestry, conversion_scale)
# Array of 3; Blender needs arrays of 4
bpycurve = bpy.data.curves.new("LineSet", 'CURVE')
bpycurve.dimensions = '3D'
@ -2467,7 +2467,7 @@ def importMesh_IndexedLineSet(geom, ancestry):
# coord = geom.getChildByName('coord') # 'Coordinate'
coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml
if coord:
points = coord.getFieldAsArray('point', 3, ancestry, conversionScale)
points = coord.getFieldAsArray('point', 3, ancestry, conversion_scale)
else:
points = []
@ -2510,7 +2510,7 @@ def importMesh_PointSet(geom, ancestry):
# VRML not x3d
coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml
if coord:
points = coord.getFieldAsArray('point', 3, ancestry, conversionScale)
points = coord.getFieldAsArray('point', 3, ancestry, conversion_scale)
else:
points = []
@ -2540,7 +2540,7 @@ def importMesh_Sphere(geom, ancestry):
# solid is ignored.
# Extra field 'subdivision="n m"' attribute, specifying how many
# rings and segments to use (X3DOM).
r = geom.getFieldAsFloat('radius', 0.5, ancestry, conversionScale)
r = geom.getFieldAsFloat('radius', 0.5*conversion_scale, ancestry, conversion_scale)
subdiv = geom.getFieldAsArray('subdivision', 0, ancestry)
if subdiv:
if len(subdiv) == 1:
@ -2635,8 +2635,8 @@ def importMesh_Cylinder(geom, ancestry):
# solid is ignored
# no ccw in this element
# Extra parameter subdivision="n" - how many faces to use
radius = geom.getFieldAsFloat('radius', 1.0, ancestry, conversionScale)
height = geom.getFieldAsFloat('height', 2, ancestry, conversionScale)
radius = geom.getFieldAsFloat('radius', 1.0*conversion_scale, ancestry, conversion_scale)
height = geom.getFieldAsFloat('height', 2.0*conversion_scale, ancestry, conversion_scale)
bottom = geom.getFieldAsBool('bottom', True, ancestry)
side = geom.getFieldAsBool('side', True, ancestry)
top = geom.getFieldAsBool('top', True, ancestry)
@ -2693,8 +2693,8 @@ def importMesh_Cone(geom, ancestry):
# Solid ignored
# Extra parameter subdivision="n" - how many faces to use
n = geom.getFieldAsInt('subdivision', GLOBALS['CIRCLE_DETAIL'], ancestry)
radius = geom.getFieldAsFloat('bottomRadius', 1.0, ancestry, conversionScale)
height = geom.getFieldAsFloat('height', 2, ancestry, conversionScale)
radius = geom.getFieldAsFloat('bottomRadius', 1.0*conversion_scale, ancestry, conversion_scale)
height = geom.getFieldAsFloat('height', 2.0*conversion_scale, ancestry, conversion_scale)
bottom = geom.getFieldAsBool('bottom', True, ancestry)
side = geom.getFieldAsBool('side', True, ancestry)
@ -2733,7 +2733,7 @@ def importMesh_Cone(geom, ancestry):
def importMesh_Box(geom, ancestry):
# Solid is ignored
# No ccw in this element
(dx, dy, dz) = geom.getFieldAsFloatTuple('size', (2.0, 2.0, 2.0), ancestry, conversionScale)
(dx, dy, dz) = geom.getFieldAsFloatTuple('size', (2.0*conversion_scale, 2.0*conversion_scale, 2.0*conversion_scale), ancestry, conversion_scale)
dx /= 2
dy /= 2
dz /= 2
@ -3286,9 +3286,9 @@ def importLamp_PointLight(node, ancestry):
# attenuation = node.getFieldAsFloatTuple('attenuation', (1.0, 0.0, 0.0), ancestry) # TODO
color = node.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0), ancestry)
intensity = node.getFieldAsFloat('intensity', 1.0, ancestry) # max is documented to be 1.0 but some files have higher.
location = node.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0), ancestry, conversionScale)
location = node.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0), ancestry, conversion_scale)
# is_on = node.getFieldAsBool('on', True, ancestry) # TODO
radius = node.getFieldAsFloat('radius', 100.0, ancestry, conversionScale)
radius = node.getFieldAsFloat('radius', 100.0, ancestry, conversion_scale)
bpylamp = bpy.data.lights.new(vrmlname, 'POINT')
bpylamp.energy = intensity
@ -3335,9 +3335,9 @@ def importLamp_SpotLight(node, ancestry):
cutOffAngle = node.getFieldAsFloat('cutOffAngle', 0.785398, ancestry) * 2.0 # max is documented to be 1.0 but some files have higher.
direction = node.getFieldAsFloatTuple('direction', (0.0, 0.0, -1.0), ancestry)
intensity = node.getFieldAsFloat('intensity', 1.0, ancestry) # max is documented to be 1.0 but some files have higher.
location = node.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0), ancestry, conversionScale)
location = node.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0), ancestry, conversion_scale)
# is_on = node.getFieldAsBool('on', True, ancestry) # TODO
radius = node.getFieldAsFloat('radius', 100.0, ancestry, conversionScale)
radius = node.getFieldAsFloat('radius', 100.0, ancestry, conversion_scale)
bpylamp = bpy.data.lights.new(vrmlname, 'SPOT')
bpylamp.energy = intensity
@ -3389,7 +3389,7 @@ def importViewpoint(bpycollection, node, ancestry, global_matrix):
fieldOfView = node.getFieldAsFloat('fieldOfView', 0.785398, ancestry) # max is documented to be 1.0 but some files have higher.
# jump = node.getFieldAsBool('jump', True, ancestry)
orientation = node.getFieldAsFloatTuple('orientation', (0.0, 0.0, 1.0, 0.0), ancestry)
position = node.getFieldAsFloatTuple('position', (0.0, 0.0, 0.0), ancestry, conversionScale)
position = node.getFieldAsFloatTuple('position', (0.0, 0.0, 0.0), ancestry, conversion_scale)
description = node.getFieldAsString('description', '', ancestry)
bpycam = bpy.data.cameras.new(name)
@ -3610,13 +3610,8 @@ def load_web3d(
# Used when adding blender primitives
GLOBALS['CIRCLE_DETAIL'] = PREF_CIRCLE_DIV
global conversionScale
units_factor = {'M': 1.0, 'DM': 0.1, 'CM': 0.01, 'MM': 0.001, 'IN': 0.0254}
if file_unit == 'CUSTOM':
conversionScale = global_scale
else:
conversionScale = units_factor[file_unit]
global conversion_scale
Bujus_Krachus marked this conversation as resolved Outdated

use snake_case

use snake_case
conversion_scale = global_scale
Hombre57 marked this conversation as resolved Outdated

uppercase for readability as it seems to be a constant.

uppercase for readability as it seems to be a constant.

Done

Done
# NOTE - reset material cache
# (otherwise we might get "StructRNA of type Material has been removed" errors)
@ -3754,7 +3749,6 @@ def load_with_profiler(
def load(context,
filepath,
*,
file_unit='M',
global_scale=1.0,
global_matrix=None
):
@ -3763,7 +3757,6 @@ def load(context,
load_web3d(context, filepath,
PREF_FLAT=True,
PREF_CIRCLE_DIV=16,
file_unit=file_unit,
global_scale=global_scale,
global_matrix=global_matrix,
)