PixelWeaver v0.5.0 /server.src.pixelweaver.character_ops
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

python
def _empty_doc() -> dict[str, Any]

The empty character-preview document (mirrors client emptyDoc).

#_live_doc

python
def _live_doc(project: ProjectState) -> dict[str, Any]

A deep, mutable copy of the live document (empty when absent).

#_commit

python
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

python
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

python
def _part_by_key(project: ProjectState, part_key: str) -> dict[str, Any] | None

The registry entry whose id == part_key (mirrors client getPart).

#_lowest_variant_index

python
def _lowest_variant_index(project: ProjectState, part: dict[str, Any]) -> int | None

The 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

python
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

python
def _assert_palette_eligible(project: ProjectState, part: dict[str, Any], class_id: str) -> None

Faithful 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

python
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

python
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

python
def _set_nested_weight(table: dict[str, dict[str, float]], outer: str, inner: str, weight: float) -> None

Set 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

python
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

python
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

python
def set_randomizer_seed(project: ProjectState, *, seed: int) -> dict[str, Any]

Set the randomizer seed. Return an undo record.

#set_variant_weight

python
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

python
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

python
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

python
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

python
def remove_osc_function(project: ProjectState, *, fn_id: str) -> dict[str, Any]

Remove an oscillator function by id (must be unassigned).

#_assert_no_conflict

python
def _assert_no_conflict(doc: dict[str, Any], fn: dict[str, Any], assignment: dict[str, Any]) -> None

Conflict rule 4.1: at most one sine per axis and one blink per resolved layer (mirrors the client assertNoConflict).

#assign_oscillator

python
def assign_oscillator(project: ProjectState, *, assignment: dict[str, Any]) -> dict[str, Any]

Add an oscillator assignment (mirrors oscillator_assign validation).

#unassign_oscillator

python
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).

Search