Skip to content

Commit

Permalink
Closes #648 and #332, relative path support in TEXTURE fields is now …
Browse files Browse the repository at this point in the history
…more sensible
  • Loading branch information
tngreene committed Nov 28, 2021
1 parent ccd736b commit 058585c
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 52 deletions.
168 changes: 117 additions & 51 deletions io_xplane2blender/xplane_types/xplane_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import platform
import re
from collections import OrderedDict
from pathlib import Path
from typing import List

import bpy
Expand Down Expand Up @@ -199,25 +200,34 @@ def _init(self):

# standard textures
if self.xplaneFile.options.texture != "":
self.attributes["TEXTURE"].setValue(
self.getPathRelativeToOBJ(
self.xplaneFile.options.texture, exportdir, blenddir
try:
self.attributes["TEXTURE"].setValue(
self.get_path_relative_to_dir(
self.xplaneFile.options.texture, exportdir
)
)
)
except (OSError, ValueError):
pass

if self.xplaneFile.options.texture_lit != "":
self.attributes["TEXTURE_LIT"].setValue(
self.getPathRelativeToOBJ(
self.xplaneFile.options.texture_lit, exportdir, blenddir
try:
self.attributes["TEXTURE_LIT"].setValue(
self.get_path_relative_to_dir(
self.xplaneFile.options.texture_lit, exportdir
)
)
)
except (OSError, ValueError):
pass

if self.xplaneFile.options.texture_normal != "":
self.attributes["TEXTURE_NORMAL"].setValue(
self.getPathRelativeToOBJ(
self.xplaneFile.options.texture_normal, exportdir, blenddir
try:
self.attributes["TEXTURE_NORMAL"].setValue(
self.get_path_relative_to_dir(
self.xplaneFile.options.texture_normal, exportdir
)
)
)
except (OSError, ValueError):
pass

xplane_version = int(bpy.context.scene.xplane.version)
if xplane_version >= 1100:
Expand Down Expand Up @@ -254,24 +264,29 @@ def _init(self):
if canHaveDraped:
# draped textures
if self.xplaneFile.options.texture_draped != "":
self.attributes["TEXTURE_DRAPED"].setValue(
self.getPathRelativeToOBJ(
self.xplaneFile.options.texture_draped, exportdir, blenddir
try:
self.attributes["TEXTURE_DRAPED"].setValue(
self.get_path_relative_to_dir(
self.xplaneFile.options.texture_draped, exportdir
)
)
)
except (OSError, ValueError):
pass

if self.xplaneFile.options.texture_draped_normal != "":
# Special "1.0" required by X-Plane
# "That's the scaling factor for the normal map available ONLY for the draped info. Without that , it can't find the texture.
# That makes a non-fatal error in x-plane. Without the normal map, the metalness directive is ignored" -Ben Supnik, 07/06/17 8:35pm
self.attributes["TEXTURE_DRAPED_NORMAL"].setValue(
"1.0 "
+ self.getPathRelativeToOBJ(
self.xplaneFile.options.texture_draped_normal,
exportdir,
blenddir,
try:
self.attributes["TEXTURE_DRAPED_NORMAL"].setValue(
"1.0 "
+ self.get_path_relative_to_dir(
self.xplaneFile.options.texture_draped_normal,
exportdir,
)
)
)
except (OSError, ValueError):
pass

if self.xplaneFile.referenceMaterials[1]:
mat = self.xplaneFile.referenceMaterials[1]
Expand Down Expand Up @@ -340,10 +355,14 @@ def _init(self):
)

if xplane_version >= 1130:
if self.xplaneFile.options.particle_system_file:
blenddir = os.path.dirname(bpy.context.blend_data.filepath)
try:
pss = self.get_path_relative_to_dir(
self.xplaneFile.options.particle_system_file, exportdir
)
except (OSError, ValueError):
pss = None

# normalize the exporpath
if self.xplaneFile.options.particle_system_file and pss:
if os.path.isabs(self.xplaneFile.filename):
exportdir = os.path.dirname(
os.path.normpath(self.xplaneFile.filename)
Expand All @@ -356,9 +375,6 @@ def _init(self):
)
)
)
pss = self.getPathRelativeToOBJ(
self.xplaneFile.options.particle_system_file, exportdir, blenddir
)

objs = self.xplaneFile.get_xplane_objects()

Expand Down Expand Up @@ -574,30 +590,80 @@ def _getCompositeNormalTexture(self, textureNormal, textureSpecular):

return texture

# Method: getPathRelativeToOBJ
# Returns the resource path relative to the exported OBJ
#
# Parameters:
# string respath - the relative or absolute resource path (such as a texture or .pss file) as chosen by the user
# string exportdir - the absolute export directory
# string blenddir - the absolute path to the directory the blend is in
#
# Returns:
# string - the resource path relative to the exported OBJ
def getPathRelativeToOBJ(self, respath: str, exportdir: str, blenddir: str) -> str:
# blender stores relative paths on UNIX with leading double slash
if respath[0:2] == "//":
respath = respath[2:]

if os.path.isabs(respath):
respath = os.path.abspath(os.path.normpath(respath))
else:
respath = os.path.abspath(os.path.normpath(os.path.join(blenddir, respath)))
def get_path_relative_to_dir(self, res_path: str, export_dir: str) -> str:
"""
Returns the resource path relative to the exported OBJ
respath = os.path.relpath(respath, exportdir)
res_path - The relative or absolute resource path (such as .png, .dds, or .pss)
as found in an RNA field
export_dir - Absolute path to directory of OBJ export
# Replace any \ separators if you're on Windows. For other platforms this does nothing
return respath.replace("\\", "/")
Raises ValueError or OSError for invalid paths or use of `//` not at the start of the respath
"""
res_path = res_path.strip()
if res_path.startswith("./") or res_path.startswith(".\\"):
res_path = res_path.replace("./", "//").replace(".\\", "//").strip()

# 9. '//', or none means "none", empty is not written -> str.replace
if res_path == "":
raise ValueError
elif res_path == "//" or res_path == "none":
return "none"
# 2. '//' is the .blend folder or CWD if not saved, -> bpy.path.abspath if bpy.data.filename else cwd
elif res_path.startswith("//") and bpy.data.filepath:
res_path = Path(bpy.path.abspath(res_path))
elif res_path.startswith("//") and not bpy.data.filepath:
res_path = Path(".") / Path(res_path[2:])
# 7. Invalid paths are a validation error -> Path.resolve throws OSError
elif "//" in res_path and not res_path.startswith("//"):
logger.error(f"'//' is used not at the start of the path '{res_path}'")
raise ValueError
elif not Path(res_path).suffix:
logger.error(
f"Resource path '{res_path}' must be a supported file type, has no extension"
)
raise ValueError
elif Path(res_path).suffix.lower() not in {".png", ".dds", ".pss"}:
logger.error(
f"Resource path '{res_path}' must be a supported file type, is {Path(res_path).suffix}"
)
raise ValueError
else:
res_path = Path(res_path)

old_cwd = os.getcwd()
if bpy.data.filepath:
# This makes '.' the .blend file directory
os.chdir(Path(bpy.data.filepath).parent)
else:
os.chdir(Path(export_dir))

try:
# 1. '.' is CWD -> Path.resolve
# 3. All paths are given '/' sperators -> Path.resolve
# 4. '..'s are resolved, '.' is a no-op -> Path.resolve
# 5. All paths must be relative to the OBJ -> Path.relative_to(does order of args matter)?
# 7. Invalid paths are a validation error -> Path.resolve throws OSError
# 8. Paths are minimal, "./path/tex.png" is "path/tex.png" -> Path.resolve
# 10. Absolute paths are okay as long as we can make a relative path os.path.relpath
rel_path = os.path.relpath(res_path.resolve(), export_dir).replace(
"\\", "/"
)
except OSError:
logger.error(f"Path '{res_path}' is invalid")
os.chdir(old_cwd)
raise
except ValueError:
logger.error(
f"Cannot make relative path across disk drives for path '{res_path}'"
)
# 6. If not possible (different drive letter), validation error Path.relative_to ValueError
# 7. Invalid paths are a validation error -> Path.resolve throws OSError
os.chdir(old_cwd)
raise
else:
os.chdir(old_cwd)
return rel_path

# Method: _getCanonicalTexturePath
# Returns normalized (canonical) path to texture
Expand Down
3 changes: 2 additions & 1 deletion tests/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import inspect
import os
import sys
from pathlib import Path
from typing import Tuple

import bpy
Expand All @@ -26,7 +27,7 @@
from io_xplane2blender.tests import *
from io_xplane2blender.tests import test_creation_helpers

__dirname__ = os.path.dirname(__file__)
__dirname__ = Path(__file__).parent

#TI filter obj output. Define above or in the class level
#def filterLines(line:Tuple[str])->bool:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
I
800
OBJ

TEXTURE tex.png
TEXTURE_LIT textures/tex.png
TEXTURE_NORMAL ../textures/tex.png
GLOBAL_specular 0.5
TEXTURE_DRAPED textures/tex.png
TEXTURE_DRAPED_NORMAL 1.0 tex.png
POINT_COUNTS 3 0 0 3

VT 1 0 1 -0 -1 -0 0.625 1 # 0
VT 1 0 -1 -0 -1 -0 0.625 0.75 # 1
VT -1 0 1 -0 -1 -0 0.375 1 # 2

IDX 0
IDX 1
IDX 2

# 0 ROOT
# 1 Mesh: Plane
# MESH: Plane weight: 1000
# MATERIAL: Material
TRIS 0 3

# Build with Blender 2.80 (sub 75) (build b'f6cb5f54494e'). Exported with XPlane2Blender 4.1.0-beta.1+103.NO_BUILD_NUMBR
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
I
800
OBJ

TEXTURE ../../tex.png
TEXTURE_LIT none
TEXTURE_NORMAL none
GLOBAL_specular 0.5
TEXTURE_DRAPED textures/tex.png
TEXTURE_DRAPED_NORMAL 1.0 tex.png
POINT_COUNTS 3 0 0 3

VT 1 0 1 -0 -1 -0 0.625 1 # 0
VT 1 0 -1 -0 -1 -0 0.625 0.75 # 1
VT -1 0 1 -0 -1 -0 0.375 1 # 2

IDX 0
IDX 1
IDX 2

# 0 ROOT
# 1 Mesh: Plane.001
# MESH: Plane.001 weight: 1000
# MATERIAL: Material
TRIS 0 3

# Build with Blender 2.80 (sub 75) (build b'f6cb5f54494e'). Exported with XPlane2Blender 4.1.0-beta.1+103.NO_BUILD_NUMBR
43 changes: 43 additions & 0 deletions tests/xplane_types/xplane_file/resource_path_new_file.test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import inspect
import os
import sys
from pathlib import Path
from typing import Tuple

import bpy

from io_xplane2blender import xplane_config
from io_xplane2blender.tests import *
from io_xplane2blender.tests import test_creation_helpers

__dirname__ = os.path.dirname(__file__)


class TestResourcePathNewFile(XPlaneTestCase):
def test_new_file_cwd_1_and_2(self) -> None:
filename = inspect.stack()[0].function
os.chdir(__dirname__)
bpy.ops.wm.read_homefile()

col = test_creation_helpers.create_datablock_collection("cwd_1_and_2")
col.xplane.is_exportable_collection = True
col.xplane.layer.name = filename + ".obj"
col.xplane.layer.texture = "C:/tex.png"

cube = test_creation_helpers.create_datablock_mesh(
test_creation_helpers.DatablockInfo(
"MESH", "Cube", collection="cwd_1_and_2"
)
)

bpy.ops.export.xplane_obj(filepath=get_tmp_folder() + f"/{filename}")
lines = (
(Path(get_tmp_folder()) / Path(filename).with_suffix(".obj"))
.read_text()
.splitlines()
)

self.assertTrue(lines[5].split()[1], "../../../../../../tex.png")


runTestCases([TestResourcePathNewFile])
Binary file not shown.
50 changes: 50 additions & 0 deletions tests/xplane_types/xplane_file/resource_paths.test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import inspect
import os
import sys
from pathlib import Path
from typing import Tuple

import bpy

from io_xplane2blender import xplane_config
from io_xplane2blender.tests import *
from io_xplane2blender.tests import test_creation_helpers

__dirname__ = Path(__file__).parent

# ONBOARDING: I was pressed for time and couldn't deal with
# making this environment agnostic. If you're reading this because you need to
# debug this code, I'm sorry for the trouble.
@unittest.skipIf(
Path("C:/Users/Ted/XPlane2Blender/tests/xplane_types/xplane_file") != __dirname__,
"This test is environment specific. You'll need to make it agnostic or change the paths for your computer.",
)
class TestResourcePaths(XPlaneTestCase):
def test_resource_paths_cases_1_4(self) -> None:
filename = inspect.stack()[0].function

self.assertExportableRootExportEqualsFixture(
filename[5:],
os.path.join(__dirname__, "fixtures", f"{filename}.obj"),
{"TEXTURE"},
filename,
)

def test_resource_paths_cases_5_and_10(self) -> None:
filename = inspect.stack()[0].function

self.assertExportableRootExportEqualsFixture(
filename[5:],
os.path.join(__dirname__, "fixtures", f"{filename}.obj"),
{"TEXTURE"},
filename,
)

def test_errors_6_7(self) -> None:
filename = inspect.stack()[0].function

out = self.exportExportableRoot(filename[5:])
self.assertLoggerErrors(5)


runTestCases([TestResourcePaths])

0 comments on commit 058585c

Please sign in to comment.