Source code for bacommon.depiction

# Released under the MIT License. See LICENSE for details.
#
"""Depictions: self-sufficient things for clients to draw.

A depiction describes something to show -- a character's icon, a
name, a character in 3D, an image -- that a client draws natively
wherever it is hosted: a doc-ui page (see
:class:`bacommon.docui.v2.Depiction`), a page's viewer pane, and later
widgets and in-game nodes. The host decides where and how big; the
depiction decides what is drawn there.

Depictions are **self-sufficient**: each carries whatever package
references it needs, and never adds packages to whatever contains it.
Art that isn't local yet shows as a standin and upgrades in place when
it arrives; the container never waits on it.

A client that doesn't recognize a depiction's kind draws a placeholder
(an outlined box with a question mark) and warns once, since producers
should only send kinds a client understands. Any finer-grained
degradation -- a name drawing plain text when its styling is too new
for the client, say -- is each kind's own business.
"""

from __future__ import annotations  # Docs-generation hack.

from enum import Enum
from dataclasses import dataclass
from typing import Annotated, override, assert_never

from efro.dataclassio import ioprepped, IOAttrs, IOMultiType

from bacommon.assetpackage import ApverNum
from bacommon.assetspec import CubeMapTextureSpec, MeshSpec, TextureSpec


[docs] class DepictionTypeID(Enum): """Type ID for each of our subclasses.""" UNKNOWN = 'u' IMAGE = 'i' CHARACTER_ICON = 'ci' NAME = 'n' CHARACTER_VIEWER = 'cv' MESH_VIEWER = 'mv' BLANK = 'bl'
[docs] class Depiction(IOMultiType[DepictionTypeID]): """Something for a client to draw; see the module docs. Every kind carries an optional ``color_override``: one rgb color imposed on the depiction, baked in. It is the static form of the color override a host can impose live (a display node's ``color_override``, say), and a host's wins where both are set. Each kind decides what it applies to (a name's text and whatever its capsule routes it to, a character icon's main color); kinds with nothing to apply it to ignore it. A player's icon in a teams game carries its team's color this way, so it shows in that color wherever it is drawn. Every kind likewise carries ``team_coloring``: the depiction's main color is a team's and should stay its clearly dominant one. Each kind decides what that means, toning its other colors down (a character icon's highlights; the parts of a name its override isn't routed to); kinds with nothing to tone ignore it. The main color is the color override if there is one, else whatever the kind takes as its own. Hosts can also turn it on live (a display node's ``team_coloring``). (Both are declared on each kind rather than here, as this base holds no fields; they are ``co`` and ``tco`` on the wire for all.) """
[docs] @override @classmethod def get_type_id(cls) -> DepictionTypeID: raise NotImplementedError()
[docs] @override @classmethod def get_type(cls, type_id: DepictionTypeID) -> type[Depiction]: # pylint: disable=too-many-return-statements t = DepictionTypeID if type_id is t.UNKNOWN: return UnknownDepiction if type_id is t.IMAGE: return ImageDepiction if type_id is t.CHARACTER_ICON: return CharacterIconDepiction if type_id is t.NAME: return NameDepiction if type_id is t.CHARACTER_VIEWER: return CharacterViewerDepiction if type_id is t.MESH_VIEWER: return MeshViewerDepiction if type_id is t.BLANK: return BlankDepiction assert_never(type_id)
[docs] @override @classmethod def get_unknown_type_fallback(cls) -> Depiction: return UnknownDepiction()
[docs] @override @classmethod def get_type_id_storage_name(cls) -> str: return '_t'
[docs] @ioprepped @dataclass class UnknownDepiction(Depiction): """A depiction of a kind this client doesn't know. Drawn as a placeholder (an outlined box with a question mark), with a one-time warning. """
[docs] @override @classmethod def get_type_id(cls) -> DepictionTypeID: return DepictionTypeID.UNKNOWN
[docs] @ioprepped @dataclass class BlankDepiction(Depiction): """Nothing at all: an empty doc-ui viewer pane, say. Whatever its host draws around it (a viewer pane's background and frame) is what shows. A doc-ui page wanting an empty pane beside its rows shows one of these, since a page with no viewer at all gets no pane (see :attr:`bacommon.docui.v2.Page.viewer`). Clients before :data:`bacommon.docui.v2.PAGE_DRIVEN_VIEWER_BUILD` don't know it (see :func:`bacommon.docui.v2.blank_viewer`). """ #: See :class:`Depiction`. (Ignored; there's nothing to color.) color_override: tuple[float, float, float] | None = None #: See :class:`Depiction`. (Ignored, as above.) team_coloring: bool = False
[docs] @override @classmethod def get_type_id(cls) -> DepictionTypeID: return DepictionTypeID.BLANK
[docs] @ioprepped @dataclass class ImageDepiction(Depiction): """A texture, fetched on demand, optionally masked and tinted. For art that must be self-sufficient -- anything whose package the page (or other container) should not have to resolve first -- such as the main toolbar's chests. Page art that resolves with its page is better served by the :class:`bacommon.docui.v2.Image` decoration. Texture slots take a full spec or, in the compact wire form, an integer index into the index domain laid out over :attr:`packages` (as character components do); producers send the compact form, with :attr:`domain_digest` guarding the indices. """ texture: TextureSpec | int #: Width over height. Stated here rather than taken from the loaded #: texture so the standin shown until the art arrives has the same #: shape, and nothing shifts when it does. aspect: float = 1.0 #: Draws the image this much bigger or smaller about its box's #: center, for art with glows or other extras that should spill #: past where it is placed. Only the drawing changes: the box (and #: so layout and what counts as a press) stays put. Clients clamp #: this to 0-2; ones before this field draw at 1. scale: float = 1.0 color: tuple[float, float, float, float] | None = None mask_texture: TextureSpec | int | None = None tint_texture: TextureSpec | int | None = None tint_color: tuple[float, float, float] | None = None tint2_color: tuple[float, float, float] | None = None #: Tint through the tint texture's blue channel (as #: :attr:`tint_color` is red and :attr:`tint2_color` green). Clients #: before this field ignore it. tint3_color: tuple[float, float, float] | None = None #: Packages laid out (in order) as the index domain integer texture #: slots index into; None when every slot is a full spec. (A plain #: None default rather than a list factory: a factory default breaks #: Sphinx's signature rendering of this class.) packages: list[ApverNum] | None = None #: Producer's (prefix) digest of that domain, checked by the client #: before resolving indices (see #: :func:`bacommon.assetspec.wire_digest`). domain_digest: str | None = ( None ) #: See :class:`Depiction`. (Images ignore it for now.) color_override: tuple[float, float, float] | None = None #: See :class:`Depiction`. team_coloring: bool = False
[docs] @override @classmethod def get_type_id(cls) -> DepictionTypeID: return DepictionTypeID.IMAGE
[docs] @ioprepped @dataclass class CharacterIconDepiction(Depiction): """A character's icon, drawn square. ``icon`` is an opaque, cloud-composed icon definition (a character's icon component on its own; the schema is server-private, see bamaster ``baserver/character.py``), tinted by its own colors. A standard-character standin shows while the art isn't local. A color override replaces the icon's main color, the way a team's color replaces a character's own on its spaz; highlights stay the icon's own. """ icon: str #: See :class:`Depiction`. color_override: tuple[float, float, float] | None = None #: See :class:`Depiction`. team_coloring: bool = False
[docs] @override @classmethod def get_type_id(cls) -> DepictionTypeID: return DepictionTypeID.CHARACTER_ICON
[docs] @ioprepped @dataclass class NameDepiction(Depiction): """A name: an in-game name, an account name, a character's name. ``name`` is an opaque, cloud-composed name definition (the same form a character's name component takes; the schema is server-private, see bamaster ``baserver/character.py``). Its basic form is colored text; richer forms (a capsule with an icon, as account names are shown) fall back to it on clients that don't understand them. Its shape is its measured text (or capsule), fitted into the box by the alignment. """ name: str #: See :class:`Depiction`. color_override: tuple[float, float, float] | None = None #: See :class:`Depiction`. team_coloring: bool = False
[docs] @override @classmethod def get_type_id(cls) -> DepictionTypeID: return DepictionTypeID.NAME
[docs] @ioprepped @dataclass class CharacterViewerDepiction(Depiction): """A character standing in a little scene of its own, live in 3D. ``spaz`` is an opaque, cloud-composed spaz definition (a character's in-game form on its own; see :class:`CharacterIconDepiction`); it alone decides how the character looks, colors included. A host keeps the viewer running across updates, so a changed definition (an editor's draft, say) updates the picture in place rather than starting it over. """ spaz: str #: How much the camera's hand-held wobble moves, relative to the #: usual: 2.0 wobbles twice as much, 0 (or less) holds it still, and #: 10 is the most there is (more counts as 10). (Still regardless #: while the viewer has camera shake disabled.) camera_shake: float = 1.0 #: How far turning the device (on devices with a gyro) swings the #: camera around the character, relative to the usual: 2.0 swings #: twice as far, 0 (or less) not at all, and 10 is the most there is #: (more counts as 10). (Not at all regardless while the viewer has #: camera gyro disabled.) camera_gyro: float = 1.0 #: See :class:`Depiction`. (Viewers ignore it; the definition alone #: decides the character's colors.) color_override: tuple[float, float, float] | None = None #: See :class:`Depiction`. team_coloring: bool = False
[docs] @override @classmethod def get_type_id(cls) -> DepictionTypeID: return DepictionTypeID.CHARACTER_VIEWER
[docs] @ioprepped @dataclass class MeshViewerEntry: """One mesh shown by a :class:`MeshViewerDepiction`, and where.""" #: The mesh, as a full spec or an index into the depiction's #: :attr:`~MeshViewerDepiction.packages` domain. mesh: MeshSpec | int #: Its texture, likewise; with none it draws in :attr:`color` #: alone. texture: TextureSpec | int | None = None #: Multiplied into its texture (or its whole color, untextured): #: rgba. Alpha only shows when :attr:`transparent`. color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0) #: Whether it's blended (by :attr:`color`'s alpha and its texture's) #: rather than solid. Blended entries draw after every solid one. #: Blending and tinting don't combine: a blended entry's tint is #: ignored (with a warning). transparent: bool = False #: A cube map it reflects (a full spec or an index); None for none. reflection: CubeMapTextureSpec | int | None = None #: Multiplied into what it reflects (rgb). reflection_scale: tuple[float, float, float] = (1.0, 1.0, 1.0) #: A texture whose red, green and blue channels say where #: :attr:`tint_color`, :attr:`tint2_color` and :attr:`tint3_color` #: apply (a character's color mask, say); None for no tinting. tint_texture: TextureSpec | int | None = None #: Tint through the tint texture's red channel. tint_color: tuple[float, float, float] = (1.0, 1.0, 1.0) #: Tint through the tint texture's green channel. tint2_color: tuple[float, float, float] = (1.0, 1.0, 1.0) #: Tint through the tint texture's blue channel. tint3_color: tuple[float, float, float] = (1.0, 1.0, 1.0) #: Where it sits, relative to the others. position: tuple[float, float, float] = (0.0, 0.0, 0.0) #: How it's turned, in degrees about x, then y, then z. rotation: tuple[float, float, float] = (0.0, 0.0, 0.0) #: How it's stretched along each axis. scale: tuple[float, float, float] = (1.0, 1.0, 1.0)
[docs] @ioprepped @dataclass class MeshViewerCameraPose: """Where a :class:`MeshViewerCamera` is at some moment. In the depiction's own space: the same one its entries' :attr:`~MeshViewerEntry.position` values place them in. """ #: Where it looks from. position: tuple[float, float, float] #: What it looks at. target: tuple[float, float, float] #: Its vertical field of view, in degrees (between 0 and 180). fov_y: float
[docs] class MeshViewerCameraTypeID(Enum): """Type ID for each kind of :class:`MeshViewerCamera`.""" UNKNOWN = 'u' STILL = 's' PING_PONG = 'pp'
[docs] class MeshViewerCamera(IOMultiType[MeshViewerCameraTypeID]): """A :class:`MeshViewerDepiction`'s camera, placed by hand. Either held in one place (:class:`MeshViewerStillCamera`) or moving (:class:`MeshViewerPingPongCamera`). A client that doesn't know a kind frames things automatically, as with no camera at all. """
[docs] @override @classmethod def get_type_id(cls) -> MeshViewerCameraTypeID: raise NotImplementedError()
[docs] @override @classmethod def get_type( cls, type_id: MeshViewerCameraTypeID ) -> type[MeshViewerCamera]: t = MeshViewerCameraTypeID if type_id is t.UNKNOWN: return MeshViewerUnknownCamera if type_id is t.STILL: return MeshViewerStillCamera if type_id is t.PING_PONG: return MeshViewerPingPongCamera assert_never(type_id)
[docs] @override @classmethod def get_default_type_id(cls) -> MeshViewerCameraTypeID: # Still cameras predate kinds (they were the only camera in # 1.8.3), so they go without a type id; this must never change. return MeshViewerCameraTypeID.STILL
[docs] @override @classmethod def get_unknown_type_fallback(cls) -> MeshViewerCamera: return MeshViewerUnknownCamera()
[docs] @ioprepped @dataclass class MeshViewerUnknownCamera(MeshViewerCamera): """A camera of a kind this client doesn't know. Treated as no camera at all: things are framed automatically. """
[docs] @override @classmethod def get_type_id(cls) -> MeshViewerCameraTypeID: return MeshViewerCameraTypeID.UNKNOWN
[docs] @ioprepped @dataclass class MeshViewerStillCamera(MeshViewerCamera): """A camera held in one place. In the depiction's own space (see :class:`MeshViewerCameraPose`, whose fields these are). """ #: Where it looks from. position: tuple[float, float, float] #: What it looks at. target: tuple[float, float, float] #: Its vertical field of view, in degrees (between 0 and 180). fov_y: float
[docs] @override @classmethod def get_type_id(cls) -> MeshViewerCameraTypeID: return MeshViewerCameraTypeID.STILL
[docs] @ioprepped @dataclass class MeshViewerPingPongCamera(MeshViewerCamera): """A camera moving back and forth between two poses. It starts at :attr:`a`, sways over to :attr:`b`, and sways back, over and over: one smooth sine wave, fastest midway and easing into each turnaround (position, target and field of view all move together, in straight lines). Time only passes while the viewer is shown, and an update leaving the camera as it was carries on without starting over. """ #: Where it starts. a: MeshViewerCameraPose #: Where it goes. b: MeshViewerCameraPose #: Seconds each way. travel_time: float #: Whether it keeps going back and forth. Off, it eases over to #: :attr:`b` just once (with the same timing) and stays there. loop: bool = True
[docs] @override @classmethod def get_type_id(cls) -> MeshViewerCameraTypeID: return MeshViewerCameraTypeID.PING_PONG
[docs] @ioprepped @dataclass class MeshViewerDepiction(Depiction): """A few meshes, live in 3D and slowly turning. Its :attr:`entries` are placed relative to one another; the client frames them all to fill the picture (by the bounds meshes carry), turns them slowly, and lets them be swung around by dragging, much as the asset browser's mesh preview does. On devices with a gyro, turning the device swings the view around what the camera looks at (see :attr:`camera_gyro`). A host keeps the viewer running across updates, so changed entries reframe in place. """ entries: list[MeshViewerEntry] #: Whether it turns on its own. (When it doesn't, it always shows #: exactly its :attr:`view_yaw` to start with.) spin: bool = True #: Whether dragging on it swings it around. Off, with :attr:`spin` #: off too, it's a completely fixed view. draggable: bool = True #: How big an area it frames, relative to the bounds of everything #: in it (about the same center): 2.0 frames twice as much, so its #: meshes look half as big. Meant for shrinking (1 and up); below 1 #: crops in (and well below, the camera ends up in among them). bounds_scale: float = 1.0 #: Degrees the turntable starts turned (about the vertical axis); #: with :attr:`spin` off, the side that faces the camera. view_yaw: float = 0.0 #: Degrees the camera starts looking down at what it frames (0 head #: on, up to just short of 90 straight down; negative looks up); #: None for the usual, about 29. view_pitch: float | None = ( None ) #: A camera placed by hand, for a set shot (held still or moving; #: see :class:`MeshViewerCamera`). When given, nothing is #: framed automatically (:attr:`bounds_scale` and :attr:`view_pitch` #: don't apply) and dragging up and down does nothing. The turntable #: still turns everything about the origin (with :attr:`spin`, #: :attr:`view_yaw`, and side to side drags). camera: MeshViewerCamera | None = None #: How much the camera's hand-held wobble moves, as #: :attr:`CharacterViewerDepiction.camera_shake` does: 1.0 is the #: character viewer's usual, 0 (or less) holds it still, and 10 is #: the most there is. None for the default: 1.0 with a #: :attr:`camera` placed by hand (a set shot), else 0. camera_shake: float | None = None #: How far turning the device (on devices with a gyro) swings the #: camera around what it looks at, relative to the usual: 2.0 swings #: twice as far, 0 (or less) not at all, and 10 is the most there is #: (more counts as 10). The usual is a generous #: swing for framed views and a subtler one (a third as far) under a #: :attr:`camera` placed by hand. (Not at all regardless while the #: viewer has camera gyro disabled.) camera_gyro: float = 1.0 #: What shows behind the meshes; None for the host's usual viewer #: background (a doc-ui viewer pane's own blank color, say). background_color: tuple[float, float, float] | None = None #: Packages laid out (in order) as the index domain integer slots #: index into; None when every slot is a full spec (see #: :class:`ImageDepiction`). packages: list[ApverNum] | None = None #: Producer's (prefix) digest of that domain (see #: :class:`ImageDepiction`). domain_digest: str | None = ( None ) #: See :class:`Depiction`. (Ignored; entries carry their own #: colors.) color_override: tuple[float, float, float] | None = None #: See :class:`Depiction`. (Ignored, as above.) team_coloring: bool = False
[docs] @override @classmethod def get_type_id(cls) -> DepictionTypeID: return DepictionTypeID.MESH_VIEWER
# Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self, TypeIs) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum