On this page
Schema-neutral character-preview mutations for part selections, the weighted randomizer, and oscillators, giving the server one validated write path per command.
#server.src.pixelweaver.character_ops
#server.src.pixelweaver.character_ops
Shared, schema-neutral character-preview mutations (WI6).
The character-preview document (ProjectState.character_preview) holds the project-scoped part selections, the weighted randomizer config, and the oscillator functions + assignments. The browser dispatches granular preview_* / oscillator_* commands that each mutate one slice of this document; previously they fell through to a history-only append and were silently lost on reload.
This module is the ONE MCP-agnostic mutation path per granular command. Every function:
- takes an already-resolved ProjectState plus plain, schema-neutral values (mirroring the client command params), - performs a read-modify-write on a deep copy of the live document (an absent document is treated as the empty document, exactly like the client's emptyDoc()), - commits through ProjectState.set_character_preview so the whole document is validated by CharacterPreviewState -- an invalid mutation is a hard ValueError, never a silent partial write, - returns a meta_snapshot undo record for the character_preview attribute (before/after full document), applied by undo_history.
Signatures are intentionally plain (ProjectState + params) so a follow-up agent can build MCP tools directly on these functions.
Faithfulness to the client reducers (character-preview-state.svelte.ts and oscillator-commands.ts) is deliberate and load-bearing: the server must produce byte-identical documents so the authoritative snapshot matches what the originating client already applied locally.
#_empty_doc
def _empty_doc() -> dict[str, Any]The empty character-preview document (mirrors client emptyDoc).
#_live_doc
def _live_doc(project: ProjectState) -> dict[str, Any]A deep, mutable copy of the live document (empty when absent).
#_commit
def _commit(project: ProjectState, before: dict[str, Any] | None, doc: dict[str, Any]) -> dict[str, Any]Validate + assign the new document; return a meta-snapshot undo record.
#_effective_selection
def _effective_selection(doc: dict[str, Any], part_key: str) -> dict[str, Any]The stored selection, else the server default.
Mirrors the client effectiveSelection for the case that matters to the self-contained commands: when a selection is stored it is returned verbatim; otherwise the neutral default {variantIndex: None, paletteClassId: None, locked: False}. (The client additionally derives the lowest available variant index from the part registry when no selection is stored -- that registry-derived default only affects preview_set_part_palette / preview_set_part_lock on a never-selected part, which are NOT covered here; preview_set_part_variant overrides variantIndex explicitly, so the neutral default is faithful for it.)
#_part_by_key
def _part_by_key(project: ProjectState, part_key: str) -> dict[str, Any] | NoneThe registry entry whose id == part_key (mirrors client getPart).
#_lowest_variant_index
def _lowest_variant_index(project: ProjectState, part: dict[str, Any]) -> int | NoneThe lowest variant index available for a part, else None.
Faithful port of the client getPartVariants + effectiveSelection derivation: variants are the canvases whose (part_set, part_name) match the part, each contributing variant_index (None coerced to 0, exactly like the client's variantIndex ?? 0); the lowest wins. A part with no variant canvases yields None (a hidden default).
#_effective_selection_registry
def _effective_selection_registry(project: ProjectState, doc: dict[str, Any], part_key: str, part: dict[str, Any]) -> dict[str, Any]The stored selection, else the registry-derived default.
Mirrors the client effectiveSelection in FULL (unlike _effective_selection which uses a neutral default): a never-selected part's variantIndex is the registry-derived lowest available variant index, paletteClassId is None, and locked is False. Used by set_part_palette / set_part_lock so those commands carry over a byte-identical baseline for never-selected parts.
#_assert_palette_eligible
def _assert_palette_eligible(project: ProjectState, part: dict[str, Any], class_id: str) -> NoneFaithful port of the client assertPaletteEligible (2.3).
A palette alternative is eligible only when the part declares a palette class, a manifest resolves both the declared and the candidate class, the candidate is not reserved, and its ramp is at least as long as the declared one. Any failure is a hard ValueError (never a silent no-op).
#set_part_palette
def set_part_palette(project: ProjectState, *, part_key: str, palette_class_id: str | None) -> dict[str, Any]Set a part's palette alternative (None = declared palette). Return an undo record.
Faithful to the client preview_set_part_palette: the part must exist in the registry, a non-null class must be eligible, and the effective (registry-derived) selection carries over for a never-selected part.
#set_part_lock
def set_part_lock(project: ProjectState, *, part_key: str, locked: bool) -> dict[str, Any]Set a part's randomizer lock. Return an undo record.
Faithful to the client preview_set_part_lock: the part must exist in the registry and the effective (registry-derived) selection carries over for a never-selected part.
#_set_nested_weight
def _set_nested_weight(table: dict[str, dict[str, float]], outer: str, inner: str, weight: float) -> NoneSet one weight with the client's pruning rules (weight==1 removes the row; an emptied inner map is pruned entirely, keeping the document minimal).
#set_part_variant
def set_part_variant(project: ProjectState, *, part_key: str, variant_index: int | None) -> dict[str, Any]Set a part's chosen variant (None = hidden). Return an undo record.
Self-contained: variantIndex is set explicitly and the palette/lock fields carry over from the effective selection (default neutral), matching the client reducer exactly.
#apply_randomize
def apply_randomize(project: ProjectState, *, randomizer: dict[str, Any], rolled_parts: dict[str, Any]) -> dict[str, Any]Apply an embedded randomize outcome (client preview_randomize).
The client mints the reroll seed (crypto) and computes the roll in execute, then embeds the resulting randomizer config + rolledParts selections into params. The server cannot reproduce those, so it replays them verbatim (read-modify-write): the whole randomizer config is replaced and each rolled selection is written into parts, mirroring the client's setRandomizer + per-part setPartSelection. set_character_preview validates the whole document, so a malformed embedded outcome is a hard error.
#set_randomizer_seed
def set_randomizer_seed(project: ProjectState, *, seed: int) -> dict[str, Any]Set the randomizer seed. Return an undo record.
#set_variant_weight
def set_variant_weight(project: ProjectState, *, part_key: str, variant: str, weight: float) -> dict[str, Any]Set one variant weight (client pruning rules). Return an undo record.
#set_palette_weight
def set_palette_weight(project: ProjectState, *, part_key: str, palette_class_id: str, weight: float) -> dict[str, Any]Set one palette weight (client pruning rules). Return an undo record.
#add_osc_function
def add_osc_function(project: ProjectState, *, fn: dict[str, Any]) -> dict[str, Any]Append an oscillator function (mirrors addOscFunction).
The function id is CLIENT-generated (stabilized into params before the command is forwarded) so ids never diverge; a missing id is a hard error.
#update_osc_function
def update_osc_function(project: ProjectState, *, fn: dict[str, Any]) -> dict[str, Any]Replace an oscillator function by id (type is immutable).
#remove_osc_function
def remove_osc_function(project: ProjectState, *, fn_id: str) -> dict[str, Any]Remove an oscillator function by id (must be unassigned).
#_assert_no_conflict
def _assert_no_conflict(doc: dict[str, Any], fn: dict[str, Any], assignment: dict[str, Any]) -> NoneConflict rule 4.1: at most one sine per axis and one blink per resolved layer (mirrors the client assertNoConflict).
#assign_oscillator
def assign_oscillator(project: ProjectState, *, assignment: dict[str, Any]) -> dict[str, Any]Add an oscillator assignment (mirrors oscillator_assign validation).
#unassign_oscillator
def unassign_oscillator(project: ProjectState, *, function_id: str, part_key: str, layer_id: str | None) -> dict[str, Any]Remove an oscillator assignment (mirrors oscillator_unassign).