Skip to content

Anim Picker 2.0#646

Merged
miquelcampos merged 25 commits into
masterfrom
anim_picker_2_DEV
Jul 14, 2026
Merged

Anim Picker 2.0#646
miquelcampos merged 25 commits into
masterfrom
anim_picker_2_DEV

Conversation

@miquelcampos

Copy link
Copy Markdown
Member

Full rewrite of the Anim Picker (internal version 2.0.0).

Features: flexible/composite tab backgrounds (#108), on-canvas
background + item manipulators, dockable mode, mirror + color palette,
shape library (polygon + SVG) with drag/create-from-selection,
vector (SVG) shapes, interactive widgets (checkbox/slider/2D-slider),
control-trace auto-build, viewport pins, conditional visibility +
checkbox group master-toggle, editor undo/redo + focus-scoped hotkeys,
HDPI text/handle scaling, expand/contract spacing, and a per-window
opacity-passthrough HUD.

Docs: new Anim Picker 2.0 user documentation page + screenshots.
Backward compatible via additive optional keys; picker data stored as
clean JSON on PICKER_DATAS (breaking only for scene-node-only pickers
from very old versions — re-export once).

…loses #598

Behavior-preserving cleanup of the anim_picker module (internal version -> 2.0.0),
the base for the planned decomposition and feature work.

- Storage: picker data on the PICKER_DATAS node is now clean JSON (json.dumps/
  json.loads) instead of a stringified Python literal read back with eval(); no
  arbitrary code execution on load. BREAKING: pickers stored only in a scene node
  with no external .pkr are not migrated and must be re-exported (.pkr files and
  file-backed pickers are unaffected).
- Fix #598 "Picker shape from curve not working": store the real tab name on the
  extraction group so the round-trip survives Maya node renaming (default ->
  default1, spaces, duplicates) instead of silently dropping edited data; guard
  shape-less transforms during curves->picker; remove the json true->True replace
  hack that corrupted names/paths containing "true"; warn on unmatched tabs.
- Fix latent bugs: .format() typo, a setAttr that never hid the data node, an
  assertion that never fired on referenced nodes, an always-false dict type check.
- Python 3 only: drop __future__ imports, super(Class, self) -> super(), remove
  module-reload NameError guards.
- Logging via mgear.log instead of print/sys.stderr (user-facing menu prints kept).
- SelectionCheck migrated from Maya API 1.0 to maya.api.OpenMaya (API 2.0).
- Remove commented-out PySide2 import blocks, stale TODOs, and dead code.
…mData (Phase 2)

Behavior-preserving decomposition of the two anim_picker God-files into focused
modules, on top of the Phase 1 cleanup. Public API, importable names, Shifter's
MAYA_OVERRIDE_COLOR, and the .pkr / PICKER_DATAS formats are unchanged.

- gui.py (2361 -> 40 lines, shim) split into constants.py, scene.py, view.py,
  tab_widget.py, main_window.py.
- widgets/picker_widgets.py (2942 -> 53 lines, shim) split into widgets/graphics.py,
  widgets/picker_item.py, and widgets/dialogs/ (script_dialog, search_replace_dialog,
  handles_window, item_options, copy_paste_dialog).
- New Qt/Maya-free widgets/item_model.py (PickerItemData); PickerItem.get_data /
  set_data delegate to its to_dict / from_dict, preserving the exact schema round-trip.
- gui.py and picker_widgets.py kept as compatibility shims re-exporting the same
  objects (isinstance identity preserved). Import cycles broken with lazy imports
  (copy_paste_dialog -> PickerItem, tab_widget -> MainDockWindow).
- Curve<->picker conversion kept in view.py; picker_curves.py extraction deferred.
…nvas #108

A tab background is now an ordered list of image layers instead of a single
image. Each layer carries its own path, position (center), and size, drawn
back-to-front. The scene rect spans the union of the background layers AND the
picker items, floored at the default canvas, so pan and zoom reach all content
and backgrounds can be larger and non-proportional (the former 6000 cap is
removed).

- New Qt/Maya-free BackgroundLayer model (widgets/background_model.py) as the
  serialization authority; get_data/set_data delegate to it.
- BackgroundOptionsDialog becomes a layer manager (add/remove/reorder, per-layer
  position and size).
- Maya curve round-trip creates one image plane per layer and reads them back.
- Legacy single background/background_size pickers still load (mapped to one
  centered layer); writes emit a "backgrounds" list.
…-select

With the Background layers panel open (edit mode), background layers can be
manipulated directly on the canvas: click / shift-click / marquee-select layers,
drag the body to move, drag the bounding-box handles to scale (corner = both
axes, edge = one, Shift keeps aspect). Multi-selected layers transform together
as a group about the opposite handle; the panel selection and X/Y/W/H fields
stay in sync.

- widgets/background_transform.py: Qt/Maya-free group move/scale math
  (union_bounds, move_layers, scale_factors, scale_layers), unit-tested.
- widgets/background_manipulator.py: BackgroundManipulator controller (selection,
  hit-test, overlay paint at constant screen size, drag lifecycle).
- view.py: background-edit sub-mode routes mouse events to the manipulator and
  paints the overlay in drawForeground; panel-sync accessors.
- BackgroundOptionsDialog: multi-selection, two-way canvas/panel sync, opens and
  closes the sub-mode.
Add a reachable, working dockable picker. A new "Anim Picker (Dockable)" menu
entry launches the window as a Maya workspaceControl through the shared
mgear.core.pyqt.showDialog helper (the same path the other dockable tools use),
with a partial supplying the edit/dockable args. MainDockableWindow now forwards
edit/dockable to MainDockWindow (they were dropped, leaving is_dockable False).

Only the dockable window carries the object name, so Maya names its
workspaceControl deterministically and the floating window no longer shares that
identity -- fixing the cases where opening both swapped the opacity UI into the
docked window, depended on open order, or where closing the floating window
closed the docked one. close() only tears down the workspaceControl when the
window is dockable. The floating window remains the default.
…and left tool strip

Add roadmap 6a of the anim_picker 2.0 creation/edit UX overhaul.

- Shared Qt-free manipulator_transform (handle points, scale factors,
  rotate helpers); background_transform now re-exports from it.
- item_manipulator: bounding-box overlay with scale handles + a rotate
  handle; group scale about the opposite anchor (Shift = uniform) and
  rigid rotate about the group center; single item rotates in place.
- edit_panel: right-docked, collapsible, edit-mode-only inline editor
  for the whole selection (Transform/Appearance/Shape/Controls/Action)
  with mixed-value-aware fields; reuses PickerItem setters and dialogs.
- tool_bar: Photoshop-style left strip with an exclusive Select
  (default) / Transform tool group; the manipulator is opt-in via the
  Transform tool. add_command hook reserved for future quick commands.
- Route double-click / Options to the inline panel (modal kept as a
  fallback). Fix: drag-moving a selected group no longer collapses the
  selection to the item under the cursor.
… picker script editor

- mgear.core.pycodeeditor.PythonCodeEditor: QPlainTextEdit with a line
  number gutter, current-line highlight, Python syntax highlighting,
  configurable monospace font (Consolas default), spaces-or-tabs indent
  with configurable width, show-whitespace, convert-tabs-to-spaces, and
  smart Tab / Shift-Tab + auto-indent on Enter. Prefs persist via
  QSettings. DCC-agnostic (no Maya import) so any tool can embed it.
- anim_picker custom script / menu dialog now embeds it, with a File /
  Edit / View menu bar (open/save scripts, edit ops, convert to spaces,
  show indentation) and a font / indentation toolbar.
…te, and tool-strip commands

Roadmap 6b of the anim_picker 2.0 creation/edit UX overhaul.

- Qt-free mirror math (widgets/mirror.py: reflect position / rotation /
  handles); PickerItem.mirror_* refactored onto it.
- Persistent, realtime live mirror relationships about a symmetry axis:
  optional additive item-data keys id / mirror (old pickers load
  unchanged), lazy pair resolution + dangling cleanup, a _mirroring loop
  guard, a cached has-links flag, and mirroring on the manipulator drag,
  the plain item drag-move, and panel edits. Linked items get a pink
  dotted shape outline; a symmetry-axis guide is drawn.
- Left tool-strip quick commands with bundled mGear icons (Add,
  Duplicate, Duplicate & Mirror, Mirror Shape, Shapes); Select /
  Transform tools use default Maya icons.
- Shape library: Qt-free widgets/shape_library.py (named handle-lists,
  bundled default_shapes.json + per-user JSON in the Maya prefs dir with
  legacy migration) + a swatch-grid dialog; apply premade / save custom.
- Explicit L/R color palette bar (widgets/color_palette.py): 6 role
  swatches, click applies (partner gets the opposite side, same level),
  double / right-click edits (persisted); color is no longer auto-swapped
  on a live geometry mirror.
- Edit-mode UX: clicking an item selects it on press (single click-drag
  selects + moves) while preserving group drag, Shift/Ctrl, marquee,
  Alt control-select, and anim-mode behavior.
Add pinned picker items that lock to the viewport and ignore canvas pan
and zoom, staying at a fixed screen position and constant size (HUD-like)
- e.g. a global reset, a space switch, or a settings button locked to a
corner. A pinned item is placed by a 3x3 anchor (corners / edges / center)
plus an inward pixel offset and keeps all its normal button behavior.

- New Qt/Maya-free widgets/overlay.py: anchor_point / offset_from_anchor /
  nearest_anchor (3x3 anchor + inward offset math), standalone-tested.
- PickerItem gains pinned / anchor / offset state; set_pinned toggles
  ItemIgnoresTransformations (constant screen size) with a scale(1,-1)
  compensation so shape / text stay upright in the Y-flipped view.
- Additive optional item keys (pinned / anchor / offset), emitted only
  when pinned, so older pickers load unchanged and non-pinned items are
  unaffected.
- view._update_pinned_items re-anchors pins on every pan / zoom / resize /
  fit / load; pins are excluded from the scene extent and fit bounds via
  scene.content_bounding_rect so a HUD never enlarges the canvas or gets
  framed by reset view. In edit mode a pinned item can be dragged to set
  its offset (the anchor snaps to the nearest region).
- edit_panel Pin section (Pinned checkbox, 3x3 anchor picker, X / Y offset,
  mixed-value aware) and a tool-strip Pin quick command.
- Refactor: shared _set_tristate / _resolve_tristate panel helpers.
Add two tab-presentation improvements to the picker.

Tabs can be dragged to reorder on the tab bar in edit mode
(ContextMenuTabWidget.setMovable), the right-click Move actions kept as a
fallback; the order saves exactly as before (get_data is positional).

A View selector (Tabbed / Grid / Rows / Columns) in the character bar
switches the picker area between the one-at-a-time tabbed view and a
multi-tab tiled view that shows every tab's picker at once. Each tiled
section is a fully live, interactive picker, laid out in nested draggable
QSplitters (minimal dividers) so the user configures each picker's
relative space; no per-cell title/frame is drawn.

- New Qt/Maya-free widgets/tiled_layout.py: grid_shape(count, columns)
  -> (rows, cols), standalone-tested (near-square / vertical / horizontal).
- New widgets/tiled_view.py: TiledPickerView (borrows the live views into
  splitters, tracks the active view via a viewport click) and PickerTabArea
  (facade presenting the tabs as tabbed or tiled; routes active view / all
  views / data / fit so callers work in either mode).
- ContextMenuTabWidget.borrow_views / restore_views + shared tab_data_entry
  (one serialization authority for both presentations).
- main_window routes _current_view / selection sync / data / fit through the
  area; forces tabbed before load; hands the whole splitter to the picker
  area when the inline panel is hidden; persists the mode + grid columns via
  QSettings (never in the .pkr, so older pickers load unchanged).
…s and align tools

Interactive widgets: a picker item can be a checkbox, 1D slider or 2D slider
bound to a Maya attribute and/or a per-state script (state/value exposed as
__STATE__/__VALUE__/__X__/__Y__), reflecting the bound value on the existing
selection-change refresh. New Qt-free widget_binding + safe attr binding
handler; modern compact rendering (grooved track, fixed round handle/knob,
upright checkbox); drag-to-canvas palette (Button/Checkbox/Slider/2D/Backdrop)
with double-click-to-create; seeded "print value" default scripts; script
editor documents the widget vars and focuses the code area.

Control trace: a tool-strip Trace command builds one silhouette button per
selected control (front/side/top convex-hull projection via Qt-free
silhouette.py + OM2 shape-point extraction), colored from the control, centered
on the viewport, auto-linking _L/_R pairs as mirrors (convertRLName).

Backdrop containers: additive backdrop/title/corner_radius keys, a rounded or
straight translucent panel drawn behind the buttons with a clipped title bar,
that moves its geometrically-contained items together (nesting supported);
inner nested backdrops stay selectable via smallest-under-cursor resolution.

Align + distribute: a left-strip Align section (Qt-free alignment.py) aligns
edges/centers and distributes by bounding box, fanning a stack of overlapping
items into an evenly spaced row/column.

Also: per-item text alignment (center/top/bottom/left/right + offset) and
viewport-pinned items keep their apparent size. All persistence is additive
optional keys, so older pickers load unchanged.
The fixed-size slider / 2D-slider knob overhangs a thin track, but
WidgetGraphic.boundingRect() stopped at the item rect, so the knob's
overhang at the previous position was never repainted -- leaving a ghost
of the old knob after a drag. boundingRect now pads by the knob radius so
the whole knob (at any position) is cleared on repaint; shape() (hit
testing) stays the plain item rect.
…e + zoom level)

A picker item can now be shown only when a condition passes in animation
mode, so a busy picker declutters itself. Two condition modes:
channel-state (a Maya attribute compared to a threshold, e.g. IK controls
only when arm_L_ctl.ikBlend >= 0.5) and zoom-level (view zoom within a
range, e.g. fine detail controls only when zoomed in).

Conditions re-evaluate on demand without polling -- zoom conditions on
every zoom/fit, channel conditions on selection change and on mouse-over
the picker (like the Channel Master tool: hovering re-reads the bound
attributes so a manual or animated channel change is picked up without
focus, and also re-syncs interactive checkbox/slider widgets). There is
no per-playback-frame refresh, so a heavy rig is not slowed. Edit mode
always shows every item so a condition never blocks authoring, and a
missing/malformed condition fails open (stays visible).

New Qt-free widgets/visibility.py owns the pure evaluation (unit-testable
standalone); PickerItem bridges the safe namespaced attribute read and
setVisible; the view owns the timing and a cached "any conditioned item?"
gate, mirroring the pinned-item pattern. Authored from a new Visibility
section in the inline edit panel (mode + attribute/operator/threshold or
min/max zoom with a "capture current zoom" helper). Persisted as one
additive optional item key (visibility), emitted only when set, so older
pickers load unchanged and unconditioned items are unaffected.
…ial import

A picker item can now be a real vector shape -- smooth curves and holes,
not just a straight-line polygon or circle. Drop an .svg file on the
canvas in edit mode (or use the Import SVG command), and it becomes one
curved item at the drop position, editable like any item: select on its
silhouette, color, mirror, scale and rotate (the transform is baked into
the vector geometry, the analog of moving polygon handles).

Import is gracefully partial: the supported geometry (path with full
curve/arc commands, rect, circle, ellipse, polygon, polyline, line,
nested in g groups with transforms and viewBox) is kept, while
unsupported elements (text, images, gradients, filters, clip/mask, use,
style, script, animation) are discarded and reported. A mixed file still
imports its shapes and lists what it dropped; an all-unsupported file
creates nothing and says why; a malformed file never raises.

Vector shapes render filled or as lines of an adjustable thickness
(stroke); the mode is auto-detected from the SVG's fill/stroke on import
(line-art comes in as strokes, so it is not filled to nothing) and is
switchable per item in the Shape panel. Vector items hide the polygon
"show handles" option and show an "SVG" badge on hover instead of the
border highlight.

The SVG parser ships as a reusable, Qt/Maya-free mgear.core.svg_import
(subpaths + suggested render mode; a flip_y option for y-up views) so
other tools can use it. Persisted as one additive optional item key
(svg), so older pickers load unchanged and polygon items are unaffected;
the existing curve<->picker conversion is kept.
…lays

On a high-DPI display (4K / Retina, or Windows / Maya UI scaling at 150%
/ 200%) the picker's on-canvas item labels and edit-mode handles stayed
at their small 96-DPI size while the window chrome grew, leaving them
hard to read and hard to hit. They now scale with the display through the
same mgear.core.pyqt.dpi_scale the chrome already uses.

Scene text (item label, edit-mode handle index, backdrop title, vector
"SVG" badge) and screen-fixed affordances (point handles, item /
background transform manipulator pick + rotate radii) grow with the
display DPI. Sizes that already scale with the canvas zoom (widget groove
/ knob, backdrop bar, badge box, fit margin, polygon default) are left
as-is so nothing is enlarged twice.

Consolidated the scattered raw point-size / pixel literals into named
constants routed through one cached DPI seam. The scaling is a no-op at
100% (standard-DPI pickers render exactly as before, since dpi_scale
clamps the factor to [1x, 2x]), and the authored text size is stored
display-independently (GraphicText keeps the authored point size separate
from the DPI-scaled font), so a picker made on one monitor renders
correctly on another.
…tcuts, and expand/contract tools

Add a single snapshot-based undo/redo stack (widgets/edit_undo.py) that every
edit-mode operation records to via the view's baseline diff: add/delete/dup/
paste/cut, move/scale/rotate/align/distribute/expand/contract/nudge, color/
text/shape/point/pin/mirror/widget/backdrop/visibility/svg/control/menu edits,
and z-order. A whole gesture (drag, manipulator) is one step; restore uses the
existing item serialization (no data-format change). Replaces the move-only
undo_move_order. Adds focus-scoped keyboard shortcuts (Ctrl+Z/Shift+Z/Y, C/V/X,
D, Shift+D, Delete/Backspace, Ctrl+A, arrows, F, Esc) and keeps edit-mode
keyboard focus on the canvas so they fire after clicking an item.

Also adds the Align strip's expand/contract spacing tools (fine-tune the
spread of a selection, backed by alignment.scale_spread_offsets).
…, and create-from-selection

Extend the shape library to hold polygon and vector (SVG) shapes, split across
Polygons / SVG tabs with previews that render real curves (shared
graphics.build_vector_path). Ship 16 bundled templates (gear, face, hand, foot,
2-/4-dir arrows, rounded square/rect, heart, plus, minus, check, cross, ring,
eye, lock). Add a tool-strip library button (grouped with Add/SVG, always
enabled), drag-a-tile-to-create at the drop point, and right-click
create-from-selection (row/column, one linked item per Maya control). Save
current shape stores polygon or imported-SVG shapes (live selection) into the
matching tab. All create ops are single undo steps.

Vector items show no dead polygon handles (Toggle handles is a no-op for them);
removed the redundant right-click Options entry (item options live in the inline
panel). Extracts a shared DragTileButton (palette + shape tiles) and promotes
the vector path builder to graphics.build_vector_path.
A checkbox widget can show / hide a named group of picker items directly in the
picker (no rig attribute required). Items gain an additive 'group' tag; the
checkbox binding gains 'visibility_group' + 'visibility_invert'. The view caches
an 'any group controller?' gate, resolves each controller to whether it shows
its group (checked XOR invert), and passes a group-hidden set into each item's
evaluate_visibility(zoom, group_hidden) -- composing group AND the item's own
condition (edit mode shows all). Consistent on load and the existing selection /
hover / zoom refresh, and immediate on toggle. Authoring: a Group field in the
Visibility section and a checkbox 'Controls group' + invert, undoable via the
panel seam. Additive optional keys keep older pickers unaffected.
The editor-undo restore applied the before-snapshot via set_data, which is
partial (only sets the optional additive keys the dict carries) and so never
cleared a key the item gained since the snapshot -- undoing the addition of a
group tag / visibility condition / SVG shape / pin / widget type / backdrop /
text / controls / menus / mirror link left it behind. The restore now first
calls PickerItem.clear_keys_absent_from(data), which resets each optional key
the restored data lacks, guarded to run only when the item has that key (a plain
move / property undo does no extra work). Redo is symmetric.
… click-through

Rework the long-broken 'Enable opacity passthrough' option into a
predictable window mask. When the floating picker is transparent
(opacity < 100%) with Auto opacity off, the window is masked down to
the item buttons + tabs so every gap is see-through and click-through
to the viewport behind.

- Build the mask by rendering the view's visible items to a transparent
  QImage (view.render with the canvas background cleared, so orientation
  and strokes match the display), then a QRegion from its alpha. Cut on
  the real painted pixels, so vector/stroke SVGs and rounded backdrops
  follow their silhouette and hidden group items stay passthrough.
- Additively boost alpha before masking so thin antialiased strokes
  (arrows, open SVGs) survive createAlphaMask's coverage threshold.
- Rebuild per frame while active (via _notify_passthrough at the view's
  transform sites) so the holes track pan/zoom with no ghosting; the
  rebuild short-circuits cheaply when passthrough is off.
- Add a top-right 'move' grip to drag the window while click-through.
- Remove the old event-filter passthrough that made the whole window
  click-through and required holding Shift.
- Selection/hover item border is now a cosmetic (constant-screen-width)
  outline so it stays readable when zoomed far out.
The 'SVG' badge on a hovered vector item is an authoring cue, so it
should not appear in animation mode. Gate VectorGraphic's badge paint
on a new lazy _edit_mode_active() helper (imports __EDIT_MODE__ only
when called, so graphics.py stays importable without Maya, matching the
_dpi helper). Hovering a vector item in anim mode now shows nothing;
edit mode still shows the badge.
…, clean edges

Refine the opacity-passthrough feature into a per-window, in-UI control.

- In-UI toggle: a 'Passthrough' checkbox left of Sync Namespace, plus a
  floating one by the move grip while the character-selector row is masked
  out; both stay in sync. Checking an opaque window drops it to a transparent
  default so it engages in one click (last transparency remembered); Auto
  opacity is turned off (mutually exclusive).
- Per-window: replace the global mgear_ap_passthrough_OV option var + the
  'Enable opacity passthrough' menu item with a per-instance flag, so enabling
  passthrough on one open picker no longer affects the others.
- Motion: during a pan / zoom the mask is dropped (the full window returns and
  moves smoothly) and re-applied once motion settles via a short timer, instead
  of reshaping the window every frame -- fixes the redraw glitch.
- Edges: erode the 1-bit mask by 1px so the antialiased item edge (blended into
  the dark canvas) no longer shows as a dark hairline.
New Sphinx page (animPickerUserDocumentation.rst, linked from the index)
covering the 2.0 rewrite: opening modes, the animation window + opacity
passthrough, edit mode (tool strip, auto-build-from-rig trace, align,
drag-to-add), the shape library, and every Item Editor panel (transform,
appearance, shape, controls, action, widgets, backdrop, visibility, pin,
mirror), plus the palette and .pkr / PICKER_DATAS storage. Ships the 22
screenshots under images/animpicker/.
…/065/066)

Behavior-preserving cleanup of the passthrough control / motion code.

- SB-066: drop the _wheel_zooming flag -- a wheel flags itself as a brief zoom
  (zoom_active, saved/restored), and one _view_in_motion() helper is the single
  'mid-gesture' check for pan / drag-zoom / wheel.
- SB-065: extract _apply_passthrough_opacity() so _toggle_passthrough reads as
  intent and the opacity-slider / auto-button coupling lives in one helper.
- SB-064: unify the in-row and floating passthrough checkboxes' creation
  through a _make_passthrough_checkbox() factory (both mirror the single
  _passthrough_enabled state).
@miquelcampos miquelcampos self-assigned this Jul 14, 2026
@github-project-automation github-project-automation Bot moved this to To Do in mGear DEV Jul 14, 2026
@miquelcampos miquelcampos moved this from To Do to WIP in mGear DEV Jul 14, 2026
@miquelcampos miquelcampos merged commit 57d3418 into master Jul 14, 2026
0 of 3 checks passed
@github-project-automation github-project-automation Bot moved this from WIP to 5.4.0 in mGear DEV Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 5.4.0

Development

Successfully merging this pull request may close these issues.

1 participant