Skip to content

Commit

Permalink
Implement new model loading class
Browse files Browse the repository at this point in the history
  • Loading branch information
object-Object committed May 12, 2024
1 parent dd54f03 commit 662a25c
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 9 deletions.
2 changes: 2 additions & 0 deletions src/hexdoc/core/properties/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"PNGTextureOverride",
"Properties",
"TemplateProps",
"TextureOverrides",
"TextureTextureOverride",
"TexturesProps",
"env",
Expand All @@ -25,6 +26,7 @@
AnimatedTexturesProps,
AnimationFormat,
PNGTextureOverride,
TextureOverrides,
TexturesProps,
TextureTextureOverride,
)
29 changes: 26 additions & 3 deletions src/hexdoc/core/properties/textures.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Annotated, Literal

from pydantic import Field
from typing_extensions import deprecated

from hexdoc.model.strip_hidden import StripHiddenModel
from hexdoc.utils.types import PydanticURL
Expand All @@ -29,6 +30,14 @@ class AnimationFormat(StrEnum):
APNG = "apng"
GIF = "gif"

@property
def suffix(self):
match self:
case AnimationFormat.APNG:
return ".png"
case AnimationFormat.GIF:
return ".gif"


class AnimatedTexturesProps(StripHiddenModel):
enabled: bool = True
Expand Down Expand Up @@ -56,8 +65,16 @@ class AnimatedTexturesProps(StripHiddenModel):
"""


class OverridesProps(StripHiddenModel):
pass
class TextureOverrides(StripHiddenModel):
models: dict[ResourceLocation, ResourceLocation | PydanticURL] = Field(
default_factory=dict
)
"""Model overrides.
Key: model id (eg. `minecraft:item/stick`).
Value: texture id (eg. `minecraft:textures/item/stick.png`) or image URL.
"""


class TexturesProps(StripHiddenModel):
Expand All @@ -73,7 +90,13 @@ class TexturesProps(StripHiddenModel):

animated: AnimatedTexturesProps = Field(default_factory=AnimatedTexturesProps)

overrides: TextureOverrides = Field(default_factory=TextureOverrides)

override: dict[
ResourceLocation,
PNGTextureOverride | TextureTextureOverride,
] = Field(default_factory=dict)
] = Field(
default_factory=dict,
deprecated=deprecated("Use textures.overrides.model instead"),
)
"""DEPRECATED."""
8 changes: 3 additions & 5 deletions src/hexdoc/graphics/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,27 +135,25 @@ def _load_layers(self, model: BlockModel):

def _save_animation(self, output_path: Path, frames: list[Image.Image]):
kwargs: dict[str, Any]
match self.texture_props.animated.format:
match output_format := self.texture_props.animated.format:
case AnimationFormat.APNG:
suffix = ".png"
kwargs = dict(
disposal=APNGDisposal.OP_BACKGROUND,
)
case AnimationFormat.GIF:
suffix = ".gif"
kwargs = dict(
loop=0, # loop forever
disposal=2, # restore to background color
)

frames[0].save(
output_path.with_suffix(suffix),
output_path.with_suffix(output_format.suffix),
save_all=True,
append_images=frames[1:],
duration=1000 / 20,
**kwargs,
)
return suffix
return output_format.suffix

def destroy(self):
self.window.destroy()
Expand Down
149 changes: 149 additions & 0 deletions src/hexdoc/minecraft/model_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import logging
import shutil
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from pathlib import Path

from typing_extensions import override
from yarl import URL

from hexdoc.core import ModResourceLoader, ResourceLocation
from hexdoc.graphics import ModelRenderer

from .model import BlockModel

logger = logging.getLogger(__name__)


@dataclass(kw_only=True)
class ModelLoader:
loader: ModResourceLoader
renderer: ModelRenderer
site_dir: Path
site_url: URL

def __post_init__(self):
self._cache = dict[ResourceLocation, URL]()

self._strategies: list[ModelLoaderStrategy] = [
FromProps(self),
FromResources(self, internal=True),
FromRenderer(self),
FromResources(self, internal=False),
]

@property
def props(self):
return self.loader.props

def render_block(self, block_id: ResourceLocation):
return self.render_model("block" / block_id)

def render_item(self, item_id: ResourceLocation):
return self.render_model("item" / item_id)

def render_model(self, model_id: ResourceLocation):
if result := self._cache.get(model_id):
logger.debug(f"Cache hit: {model_id} = {result}")
return result

_, model = BlockModel.load_and_resolve(self.loader, model_id)
for override_id, override_model in self._get_overrides(model_id, model):
for strategy in self._strategies:
try:
if result := strategy(override_id, override_model):
self._cache[model_id] = self._cache[override_id] = result
return result
except Exception:
logger.debug(
f"Exception while rendering override: {override_id}",
exc_info=True,
)

message = f"All strategies failed to render model: {model_id}"
if self.props.textures.strict:
raise ValueError(message)
logger.error(message)

def _get_overrides(self, model_id: ResourceLocation, model: BlockModel):
# TODO: implement
yield model_id, model


@dataclass
class ModelLoaderStrategy(ABC):
ml: ModelLoader = field(repr=False)

model_id: ResourceLocation = field(init=False, repr=False)
model: BlockModel = field(init=False, repr=False)

def __call__(self, model_id: ResourceLocation, model: BlockModel) -> URL | None:
logger.debug(f"Attempting strategy: {self}")
self.model_id = model_id
self.model = model
return self._execute()

@abstractmethod
def _execute(self) -> URL | None: ...

def _from_existing_image(self, src: Path):
fragment = self._get_fragment(src.suffix)
shutil.copyfile(src, self.ml.site_dir / fragment)
return self._fragment_to_url(fragment)

def _get_fragment(self, suffix: str = ".png"):
path = Path("renders") / self.model_id.namespace / self.model_id.path
return path.with_suffix(suffix)

def _fragment_to_url(self, fragment: Path):
return self.ml.site_url.joinpath(*fragment.parts)


class FromProps(ModelLoaderStrategy):
@override
def _execute(self) -> URL | None:
match self.ml.props.textures.overrides.models.get(self.model_id):
case ResourceLocation() as texture_id:
_, src = self.ml.loader.find_resource("assets", "", texture_id)
return self._from_existing_image(src)
case URL() as url:
return url
case None:
logger.debug(f"No props override for model: {self.model_id}")
return None


@dataclass
class FromResources(ModelLoaderStrategy):
internal: bool

@override
def _execute(self) -> URL | None:
preferred_suffix = self.ml.props.textures.animated.format.suffix

src = None
for resource_dir, _, path in self.ml.loader.find_resources(
"assets",
namespace=self.model_id.namespace,
folder="hexdoc/renders",
glob=self.model_id.path + ".{png,gif}",
allow_missing=True,
):
if resource_dir.internal == self.internal:
src = path
if path.suffix == preferred_suffix:
break

if src:
return self._from_existing_image(src)

type_ = "internal" if self.internal else "external"
logger.debug(f"No {type_} rendered resource for model: {self.model_id}")


class FromRenderer(ModelLoaderStrategy):
@override
def _execute(self) -> URL | None:
fragment = self._get_fragment()
suffix = self.ml.renderer.render_model(self.model, self.ml.site_dir / fragment)
return self._fragment_to_url(fragment.with_suffix(suffix))
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import CodeBlock from "@theme/CodeBlock";
1. `hexdoc.toml`:
<CodeBlock language="toml" title="doc/hexdoc.toml">
{[
"[textures.override.model]",
"[textures.overrides.models]",
`${props.namespace}:${props.path}" = ...`,
].join("\n")}
</CodeBlock>
Expand Down

0 comments on commit 662a25c

Please sign in to comment.