from __future__ import annotations from enum import Enum from typing import Iterable, List, Literal, Tuple, TypedDict, Union, TYPE_CHECKING from base_types import InputId from nodes.group import NestedGroup, group if TYPE_CHECKING: import navi from nodes.base_input import BaseInput InputValue = Union[int, str] EnumValues = Union[ InputValue, Enum, Iterable[str], Iterable[int], Iterable[Enum], ] RawEnumValues = Union[ InputValue, List[str], List[int], Tuple[str, ...], Tuple[int, ...] ] _ConditionJson = Union[ "_AndConditionJson", "_OrConditionJson", "_NotConditionJson", "_EnumConditionJson", "_TypeConditionJson", ] class _AndConditionJson(TypedDict): kind: Literal["and"] items: List[_ConditionJson] class _OrConditionJson(TypedDict): kind: Literal["or"] items: List[_ConditionJson] class _NotConditionJson(TypedDict): kind: Literal["not"] condition: _ConditionJson class _EnumConditionJson(TypedDict): kind: Literal["enum"] enum: InputId values: List[str | int] class _TypeConditionJson(TypedDict): kind: Literal["type"] input: InputId condition: navi.ExpressionJson ifNotConnected: bool class Condition: def __init__(self, value: _ConditionJson) -> None: self._value: _ConditionJson = value def to_json(self): return self._value def __and__(self, other: Condition) -> Condition: return Condition({"kind": "and", "items": [self._value, other._value]}) def __or__(self, other: Condition) -> Condition: return Condition({"kind": "or", "items": [self._value, other._value]}) def __invert__(self) -> Condition: return Condition({"kind": "not", "condition": self._value}) @staticmethod def enum(enum: int, values: EnumValues) -> Condition: """ A condition to check whether a certain dropdown/enum input has a certain value. """ v: List[str | int] = [] def convert(value: int | str | Enum): if isinstance(value, (int, str)): v.append(value) else: enum_value = value.value assert isinstance(enum_value, (int, str)) v.append(enum_value) if isinstance(values, (int, str, Enum)): convert(values) else: for value in values: convert(value) return Condition( { "kind": "enum", "enum": InputId(enum), "values": v, } ) @staticmethod def bool(input_id: int, value: bool) -> Condition: """ A condition to check whether a certain bool input has a certain value. """ return Condition( { "kind": "enum", "enum": InputId(input_id), "values": [int(value)], } ) @staticmethod def type( input_id: int, condition: navi.ExpressionJson, if_not_connected: bool = False, ) -> Condition: """ A condition to check whether a certain input is compatible a certain type. Here "compatible" is defined as overlapping. """ return Condition( { "kind": "type", "input": InputId(input_id), "condition": condition, "ifNotConnected": if_not_connected, } ) @staticmethod def const(value: bool) -> Condition: if value: return Condition({"kind": "and", "items": []}) return Condition({"kind": "or", "items": []}) def if_group(condition: Condition): return group("conditional", {"condition": condition.to_json()}) def if_enum_group(enum: int, condition: EnumValues): return if_group(Condition.enum(enum, condition)) def required(condition: Condition | None = None): """ Given generic inputs (meaning of kind "generic") that are optional, this group marks them as being required under the given condition. If no condition is given, `True` will be used. In addition to the given condition, if the require group is nested within conditional group (`if_group` and derivatives), then the conditions of all ancestor conditional groups must also be met. Note that this group only guarantees **best effort**. It cannot guarantee that the optional input is going to have a value if the condition is met. You must always check `None`. Example: ```py if_group(someCondition)( required()( GenericInput("Foo").make_optional(), ) ) ``` In this example, the input "Foo" is required if and only if the input is visible (by virtue of the parent conditional group). """ if condition is None: condition = Condition.const(True) return group("required", {"condition": condition.to_json()}) def seed_group(seed_input: BaseInput): """ This groups is a wrapper around the `SeedInput`. It changes its visual appearance and adds a little button for users to click on to generate a new seed. All `SeedInput`s must be wrapped in this group. Example: ```py seed_group(SeedInput()) ``` """ return group("seed")(seed_input) def optional_list_group(*inputs: BaseInput | NestedGroup): """ This groups wraps around optional inputs and displays them as a list. This can be used to create nodes that have a variable number of inputs. The user will initially see no inputs, but can add as many inputs as the group contains. While not true varargs, this can be used to create a similar effect. See the Text Append node for an example. """ return group("optional-list")(*inputs) def linked_inputs_group(*inputs: BaseInput): """ This group wraps around inputs of the same type. It ensures that all inputs have the same value. "The same type" here not only refers to the Navi type of those inputs. All possible values from all inputs must also be valid values for all other inputs. This typically necessitates that the inputs are of the same class and use the same parameters. """ return group("linked-inputs")(*inputs)