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 77 additions and 26 deletions

View File

@ -37,11 +37,34 @@ class ImportX3D(bpy.types.Operator, ImportHelper):
filename_ext = ".x3d" filename_ext = ".x3d"
filter_glob: StringProperty(default="*.x3d;*.wrl", options={'HIDDEN'}) filter_glob: StringProperty(default="*.x3d;*.wrl", options={'HIDDEN'})
file_unit: EnumProperty(
name="File Unit",
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"),
('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', "CUSTOM", "Use the scale factor provided below"),
),
description="Unit used in the input file",
default='M',
)
global_scale: FloatProperty(
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 'CUSTOM'",
)
def execute(self, context): def execute(self, context):
from . import import_x3d from . import import_x3d
keywords = self.as_keywords(ignore=("axis_forward", keywords = self.as_keywords(ignore=("axis_forward",
"axis_up", "axis_up",
"file_unit",
"filter_glob", "filter_glob",
)) ))
global_matrix = axis_conversion(from_forward=self.axis_forward, global_matrix = axis_conversion(from_forward=self.axis_forward,
@ -248,6 +271,15 @@ class X3D_PT_import_transform(bpy.types.Panel):
sfile = context.space_data sfile = context.space_data
operator = sfile.active_operator operator = sfile.active_operator
layout.prop(operator, "file_unit")
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_forward")
layout.prop(operator, "axis_up") layout.prop(operator, "axis_up")

View File

@ -15,6 +15,7 @@ from itertools import chain
texture_cache = {} texture_cache = {}
material_cache = {} material_cache = {}
conversion_scale = 1.0
EPSILON = 0.0000001 # Very crude. EPSILON = 0.0000001 # Very crude.
@ -681,7 +682,7 @@ class vrmlNode(object):
print('\tvalue "%s" could not be used as an int for field "%s"' % (f[0], field)) print('\tvalue "%s" could not be used as an int for field "%s"' % (f[0], field))
return default return default
def getFieldAsFloat(self, field, default, ancestry): def getFieldAsFloat(self, field, default, ancestry, scale_factor=1.0):
self_real = self.getRealNode() # in case we're an instance self_real = self.getRealNode() # in case we're an instance
f = self_real.getFieldName(field, ancestry) f = self_real.getFieldName(field, ancestry)
@ -695,12 +696,12 @@ class vrmlNode(object):
return default return default
try: try:
return float(f[0]) return float(f[0])*scale_factor
except: except:
print('\tvalue "%s" could not be used as a float for field "%s"' % (f[0], field)) print('\tvalue "%s" could not be used as a float for field "%s"' % (f[0], field))
return default return default
def getFieldAsFloatTuple(self, field, default, ancestry): def getFieldAsFloatTuple(self, field, default, ancestry, scale_factor=1.0):
self_real = self.getRealNode() # in case we're an instance self_real = self.getRealNode() # in case we're an instance
f = self_real.getFieldName(field, ancestry) f = self_real.getFieldName(field, ancestry)
@ -716,7 +717,7 @@ class vrmlNode(object):
for v in f: for v in f:
if v != ',': if v != ',':
try: try:
ret.append(float(v.strip('"'))) ret.append(float(v.strip('"'))*scale_factor)
except: 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 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) # print(ret)
@ -774,7 +775,7 @@ class vrmlNode(object):
print('\tvalue "%s" could not be used as a string for field "%s"' % (f[0], field)) print('\tvalue "%s" could not be used as a string for field "%s"' % (f[0], field))
return default return default
def getFieldAsArray(self, field, group, ancestry): def getFieldAsArray(self, field, group, ancestry, scale_factor=1.0):
""" """
For this parser arrays are children For this parser arrays are children
""" """
@ -826,9 +827,16 @@ class vrmlNode(object):
flat = False flat = False
break break
apply_scale = scale_factor != 1.0
# make a flat array # make a flat array
if flat: if flat:
if apply_scale:
# applying scale
flat_array = [n*scale_factor for n in array_data] # scaling the data
else:
flat_array = array_data # we are already flat. flat_array = array_data # we are already flat.
else: else:
flat_array = [] flat_array = []
@ -837,6 +845,10 @@ class vrmlNode(object):
if type(item) == list: if type(item) == list:
extend_flat(item) extend_flat(item)
else: else:
if apply_scale:
# applying scale
item *= scale_factor
flat_array.append(item) flat_array.append(item)
extend_flat(array_data) extend_flat(array_data)
@ -1498,11 +1510,11 @@ def translateScale(sca):
def translateTransform(node, ancestry): def translateTransform(node, ancestry):
cent = node.getFieldAsFloatTuple('center', None, ancestry) # (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) rot = node.getFieldAsFloatTuple('rotation', None, ancestry) # (0.0, 0.0, 1.0, 0.0)
sca = node.getFieldAsFloatTuple('scale', None, ancestry) # (1.0, 1.0, 1.0) sca = node.getFieldAsFloatTuple('scale', None, ancestry) # (1.0, 1.0, 1.0)
scaori = node.getFieldAsFloatTuple('scaleOrientation', None, ancestry) # (0.0, 0.0, 1.0, 0.0) scaori = node.getFieldAsFloatTuple('scaleOrientation', None, ancestry) # (0.0, 0.0, 1.0, 0.0)
tx = node.getFieldAsFloatTuple('translation', None, ancestry) # (0.0, 0.0, 0.0) tx = node.getFieldAsFloatTuple('translation', None, ancestry, conversion_scale) # (0.0, 0.0, 0.0)
if cent: if cent:
cent_mat = Matrix.Translation(cent) cent_mat = Matrix.Translation(cent)
@ -1542,10 +1554,10 @@ def translateTransform(node, ancestry):
def translateTexTransform(node, ancestry): def translateTexTransform(node, ancestry):
cent = node.getFieldAsFloatTuple('center', None, ancestry) # (0.0, 0.0) cent = node.getFieldAsFloatTuple('center', None, ancestry, conversion_scale) # (0.0, 0.0)
rot = node.getFieldAsFloat('rotation', None, ancestry) # 0.0 rot = node.getFieldAsFloat('rotation', None, ancestry) # 0.0
sca = node.getFieldAsFloatTuple('scale', None, ancestry) # (1.0, 1.0) sca = node.getFieldAsFloatTuple('scale', None, ancestry) # (1.0, 1.0)
tx = node.getFieldAsFloatTuple('translation', None, ancestry) # (0.0, 0.0) tx = node.getFieldAsFloatTuple('translation', None, ancestry, conversion_scale) # (0.0, 0.0)
if cent: if cent:
# cent is at a corner by default # cent is at a corner by default
@ -1682,7 +1694,7 @@ def importMesh_ReadVertices(bpymesh, geom, ancestry):
# IndexedFaceSet presumes a 2D one. # IndexedFaceSet presumes a 2D one.
# The case for caching is stronger over there. # The case for caching is stronger over there.
coord = geom.getChildBySpec('Coordinate') coord = geom.getChildBySpec('Coordinate')
points = coord.getFieldAsArray('point', 0, ancestry) points = coord.getFieldAsArray('point', 0, ancestry, conversion_scale)
bpymesh.vertices.add(len(points) // 3) bpymesh.vertices.add(len(points) // 3)
bpymesh.vertices.foreach_set("co", points) bpymesh.vertices.foreach_set("co", points)
@ -1919,7 +1931,7 @@ def importMesh_IndexedFaceSet(geom, ancestry):
# TODO: resolve that somehow, so that vertex set can be effectively # TODO: resolve that somehow, so that vertex set can be effectively
# reused between different mesh types? # reused between different mesh types?
else: else:
points = coord.getFieldAsArray('point', 3, ancestry) points = coord.getFieldAsArray('point', 3, ancestry, conversion_scale)
if coord.canHaveReferences(): if coord.canHaveReferences():
coord.parsed = points coord.parsed = points
index = geom.getFieldAsArray('coordIndex', 0, ancestry) index = geom.getFieldAsArray('coordIndex', 0, ancestry)
@ -2430,7 +2442,7 @@ def importMesh_LineSet(geom, ancestry):
# TODO: line display properties are ignored # TODO: line display properties are ignored
# Per-vertex color is ignored # Per-vertex color is ignored
coord = geom.getChildBySpec('Coordinate') coord = geom.getChildBySpec('Coordinate')
src_points = coord.getFieldAsArray('point', 3, ancestry) src_points = coord.getFieldAsArray('point', 3, ancestry, conversion_scale)
# Array of 3; Blender needs arrays of 4 # Array of 3; Blender needs arrays of 4
bpycurve = bpy.data.curves.new("LineSet", 'CURVE') bpycurve = bpy.data.curves.new("LineSet", 'CURVE')
bpycurve.dimensions = '3D' bpycurve.dimensions = '3D'
@ -2456,7 +2468,7 @@ def importMesh_IndexedLineSet(geom, ancestry):
# coord = geom.getChildByName('coord') # 'Coordinate' # coord = geom.getChildByName('coord') # 'Coordinate'
coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml
if coord: if coord:
points = coord.getFieldAsArray('point', 3, ancestry) points = coord.getFieldAsArray('point', 3, ancestry, conversion_scale)
else: else:
points = [] points = []
@ -2499,7 +2511,7 @@ def importMesh_PointSet(geom, ancestry):
# VRML not x3d # VRML not x3d
coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml
if coord: if coord:
points = coord.getFieldAsArray('point', 3, ancestry) points = coord.getFieldAsArray('point', 3, ancestry, conversion_scale)
else: else:
points = [] points = []
@ -2529,7 +2541,7 @@ def importMesh_Sphere(geom, ancestry):
# solid is ignored. # solid is ignored.
# Extra field 'subdivision="n m"' attribute, specifying how many # Extra field 'subdivision="n m"' attribute, specifying how many
# rings and segments to use (X3DOM). # rings and segments to use (X3DOM).
r = geom.getFieldAsFloat('radius', 0.5, ancestry) r = geom.getFieldAsFloat('radius', 0.5*conversion_scale, ancestry, conversion_scale)
subdiv = geom.getFieldAsArray('subdivision', 0, ancestry) subdiv = geom.getFieldAsArray('subdivision', 0, ancestry)
if subdiv: if subdiv:
if len(subdiv) == 1: if len(subdiv) == 1:
@ -2624,8 +2636,8 @@ def importMesh_Cylinder(geom, ancestry):
# solid is ignored # solid is ignored
# no ccw in this element # no ccw in this element
# Extra parameter subdivision="n" - how many faces to use # Extra parameter subdivision="n" - how many faces to use
radius = geom.getFieldAsFloat('radius', 1.0, ancestry) radius = geom.getFieldAsFloat('radius', 1.0*conversion_scale, ancestry, conversion_scale)
height = geom.getFieldAsFloat('height', 2, ancestry) height = geom.getFieldAsFloat('height', 2.0*conversion_scale, ancestry, conversion_scale)
bottom = geom.getFieldAsBool('bottom', True, ancestry) bottom = geom.getFieldAsBool('bottom', True, ancestry)
side = geom.getFieldAsBool('side', True, ancestry) side = geom.getFieldAsBool('side', True, ancestry)
top = geom.getFieldAsBool('top', True, ancestry) top = geom.getFieldAsBool('top', True, ancestry)
@ -2682,8 +2694,8 @@ def importMesh_Cone(geom, ancestry):
# Solid ignored # Solid ignored
# Extra parameter subdivision="n" - how many faces to use # Extra parameter subdivision="n" - how many faces to use
n = geom.getFieldAsInt('subdivision', GLOBALS['CIRCLE_DETAIL'], ancestry) n = geom.getFieldAsInt('subdivision', GLOBALS['CIRCLE_DETAIL'], ancestry)
radius = geom.getFieldAsFloat('bottomRadius', 1.0, ancestry) radius = geom.getFieldAsFloat('bottomRadius', 1.0*conversion_scale, ancestry, conversion_scale)
height = geom.getFieldAsFloat('height', 2, ancestry) height = geom.getFieldAsFloat('height', 2.0*conversion_scale, ancestry, conversion_scale)
bottom = geom.getFieldAsBool('bottom', True, ancestry) bottom = geom.getFieldAsBool('bottom', True, ancestry)
side = geom.getFieldAsBool('side', True, ancestry) side = geom.getFieldAsBool('side', True, ancestry)
@ -2722,7 +2734,7 @@ def importMesh_Cone(geom, ancestry):
def importMesh_Box(geom, ancestry): def importMesh_Box(geom, ancestry):
# Solid is ignored # Solid is ignored
# No ccw in this element # No ccw in this element
(dx, dy, dz) = geom.getFieldAsFloatTuple('size', (2.0, 2.0, 2.0), ancestry) (dx, dy, dz) = geom.getFieldAsFloatTuple('size', (2.0*conversion_scale, 2.0*conversion_scale, 2.0*conversion_scale), ancestry, conversion_scale)
dx /= 2 dx /= 2
dy /= 2 dy /= 2
dz /= 2 dz /= 2
@ -3275,9 +3287,9 @@ def importLamp_PointLight(node, ancestry):
# attenuation = node.getFieldAsFloatTuple('attenuation', (1.0, 0.0, 0.0), ancestry) # TODO # attenuation = node.getFieldAsFloatTuple('attenuation', (1.0, 0.0, 0.0), ancestry) # TODO
color = node.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0), ancestry) 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. 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) location = node.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0), ancestry, conversion_scale)
# is_on = node.getFieldAsBool('on', True, ancestry) # TODO # is_on = node.getFieldAsBool('on', True, ancestry) # TODO
radius = node.getFieldAsFloat('radius', 100.0, ancestry) radius = node.getFieldAsFloat('radius', 100.0, ancestry, conversion_scale)
bpylamp = bpy.data.lights.new(vrmlname, 'POINT') bpylamp = bpy.data.lights.new(vrmlname, 'POINT')
bpylamp.energy = intensity bpylamp.energy = intensity
@ -3324,9 +3336,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. 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) 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. 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) location = node.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0), ancestry, conversion_scale)
# is_on = node.getFieldAsBool('on', True, ancestry) # TODO # is_on = node.getFieldAsBool('on', True, ancestry) # TODO
radius = node.getFieldAsFloat('radius', 100.0, ancestry) radius = node.getFieldAsFloat('radius', 100.0, ancestry, conversion_scale)
bpylamp = bpy.data.lights.new(vrmlname, 'SPOT') bpylamp = bpy.data.lights.new(vrmlname, 'SPOT')
bpylamp.energy = intensity bpylamp.energy = intensity
@ -3378,7 +3390,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. 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) # jump = node.getFieldAsBool('jump', True, ancestry)
orientation = node.getFieldAsFloatTuple('orientation', (0.0, 0.0, 1.0, 0.0), 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) position = node.getFieldAsFloatTuple('position', (0.0, 0.0, 0.0), ancestry, conversion_scale)
description = node.getFieldAsString('description', '', ancestry) description = node.getFieldAsString('description', '', ancestry)
bpycam = bpy.data.cameras.new(name) bpycam = bpy.data.cameras.new(name)
@ -3590,6 +3602,8 @@ def load_web3d(
*, *,
PREF_FLAT=False, PREF_FLAT=False,
PREF_CIRCLE_DIV=16, PREF_CIRCLE_DIV=16,
file_unit='M',
global_scale=1.0,
global_matrix=None, global_matrix=None,
HELPER_FUNC=None HELPER_FUNC=None
): ):
@ -3597,6 +3611,9 @@ def load_web3d(
# Used when adding blender primitives # Used when adding blender primitives
GLOBALS['CIRCLE_DETAIL'] = PREF_CIRCLE_DIV GLOBALS['CIRCLE_DETAIL'] = PREF_CIRCLE_DIV
global conversion_scale
conversion_scale = global_scale
# NOTE - reset material cache # NOTE - reset material cache
# (otherwise we might get "StructRNA of type Material has been removed" errors) # (otherwise we might get "StructRNA of type Material has been removed" errors)
global material_cache global material_cache
@ -3733,6 +3750,7 @@ def load_with_profiler(
def load(context, def load(context,
filepath, filepath,
*, *,
global_scale=1.0,
global_matrix=None global_matrix=None
): ):
@ -3740,6 +3758,7 @@ def load(context,
load_web3d(context, filepath, load_web3d(context, filepath,
PREF_FLAT=True, PREF_FLAT=True,
PREF_CIRCLE_DIV=16, PREF_CIRCLE_DIV=16,
global_scale=global_scale,
global_matrix=global_matrix, global_matrix=global_matrix,
) )