Skip to content

Commit 6d2006a

Browse files
committed
Merge the effect ink, the trigger outline and the scoped rows
2 parents fef023c + ec1ccf9 commit 6d2006a

12 files changed

Lines changed: 1393 additions & 73 deletions

File tree

‎apps/studio/src/mhfu_studio/monster/behaviour.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
IN, OUT = "in", "out"
2929
PLAY, WHILE, THEN = "play", "while playing", "then"
3030
"""A move node's ports: the input, then the two outputs."""
31+
ALL = "all"
32+
"""The scope that is the whole canvas, as stored; any other is a trigger's key."""
3133
ROLE_TITLES = {"state": "State"}
3234
"""A palette group's title where the role in plural is not it."""
3335

‎apps/studio/src/mhfu_studio/monster/panels/behaviour.py‎

Lines changed: 131 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,28 @@
33
"""The Behaviour tab: the port's behaviour graph (`mhfu_port.behaviour`) on a node canvas.
44
55
A node is a move or a block, drawn compact; a wire is a link the manifest stores
6-
(`monster/behaviour.py`). The selected node's values and note are edited in the bar under the
7-
canvas. The document is the owner: every gesture of the canvas and every edit of the bar arrives
8-
as intents, is applied as one undo step, and is shown again from the document.
6+
(`monster/behaviour.py`). The outline beside the canvas scopes it: All is the canvas as stored,
7+
a trigger shows only its rules, one framed row each in the order they are checked (`rows`), laid
8+
out for display and never written to a block's `at`. The selected node's values and note are
9+
edited in the bar under the canvas. The document is the owner: every gesture of the canvas and
10+
every edit of the bar arrives as intents, is applied as one undo step, and is shown again from
11+
the document.
912
"""
1013

1114
from __future__ import annotations
1215

16+
import dataclasses
1317
from collections.abc import Callable
1418
from typing import TYPE_CHECKING
1519

1620
from mhfu_port import behaviour as model
17-
from mhfu_port.behaviour import KINDS, Kind
21+
from mhfu_port.behaviour import KINDS, Kind, Trigger
1822
from mhfu_port.manifest import Manifest, ManifestError, Move
19-
from PySide6.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
23+
from PySide6.QtWidgets import QHBoxLayout, QSizePolicy, QVBoxLayout, QWidget
2024

2125
from mhfu_studio.monster import behaviour as graph
26+
from mhfu_studio.monster.behaviour import ALL
27+
from mhfu_studio.monster.panels import rows, triggers
2228
from mhfu_studio.monster.panels.common import kind
2329
from mhfu_studio.monster.panels.node_graph import GraphView, LinkSpec, NodeSpec
2430
from mhfu_studio.monster.panels.properties import Detail, PropertyBar
@@ -36,21 +42,17 @@
3642
"state": Ink.AXIS_Y,
3743
"condition": Ink.AXIS_Z,
3844
"modifier": Ink.WARNING,
45+
"effect": Ink.EFFECT,
3946
"move": Ink.SELECTION,
4047
}
41-
#: the effect role's violet, for a dark theme and a light one: no `Ink` is one
42-
EFFECT: dict[bool, Color] = {True: (0.82, 0.44, 0.91, 1.0), False: (0.56, 0.14, 0.67, 1.0)}
4348
MOVE_ROLE = "move"
4449
WARN = chr(0x26A0)
4550
HINT = (
4651
"Right-click adds a block · drag from a port to wire · higher blocks are checked first,"
4752
" but paths that play nothing run before all of them"
4853
)
49-
50-
51-
def roles() -> dict[str, Color]:
52-
"""`ROLES` and the effect's violet, for the current theme."""
53-
return {**ROLES, "effect": EFFECT[theme.current().dark]}
54+
#: said for the intents a trigger's rows do not take yet, and over the rows
55+
SCOPED = "Create and wire in All for now"
5456

5557

5658
class BehaviourPanel(kit.Panel):
@@ -67,8 +69,11 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
6769
self._dropped: dict[str, graph.Point] = {}
6870
#: the block the gesture creates
6971
self._made: str | None = None
70-
#: what the canvas was last drawn from, for the bar
72+
#: what the canvas was last drawn from, for the bar and the outline
7173
self._read: graph.Reading | None = None
74+
self._triggers: list[Trigger] = []
75+
self._scope: str | None = None
76+
self._outlined: object = None
7277

7378
self.title = kit.label(role="title", wrap=False)
7479
self.hint = kit.label(HINT, role="muted", wrap=False)
@@ -95,20 +100,28 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
95100
self.view = GraphView()
96101
self.view.set_palette(graph.palette())
97102
v = self.view
98-
v.link_requested.connect(lambda *w: self._collect(lambda e: graph.link(e, *w)))
99-
v.unlink_requested.connect(lambda *w: self._collect(lambda e: graph.unlink(e, *w)))
100-
v.delete_requested.connect(lambda ids: self._collect(lambda e: graph.delete(e, ids)))
101-
v.moved.connect(lambda i, x, y: self._dropped.__setitem__(i, (x, y)))
103+
v.link_requested.connect(lambda *w: self._collect(self._wire(graph.link, *w)))
104+
v.unlink_requested.connect(lambda *w: self._collect(self._wire(graph.unlink, *w)))
105+
v.delete_requested.connect(self._delete)
106+
v.moved.connect(self._moved)
102107
v.create_requested.connect(self._create)
103108
v.picked.connect(self._picked)
104109
v.settled.connect(self._settle)
105110
self.bar = PropertyBar()
106111
self.bar.param_changed.connect(self._param)
107112
self.bar.label_changed.connect(self._label)
108113

114+
self.outline = triggers.Outline()
115+
self.outline.picked.connect(self._scoped)
109116
page = QWidget()
110-
lay = QVBoxLayout(page)
111-
lay.setContentsMargins(10, 8, 10, 8)
117+
side = QHBoxLayout(page)
118+
side.setContentsMargins(10, 8, 10, 8)
119+
side.setSpacing(10)
120+
side.addWidget(self.outline)
121+
main = QWidget()
122+
side.addWidget(main, 1)
123+
lay = QVBoxLayout(main)
124+
lay.setContentsMargins(0, 0, 0, 0)
112125
lay.setSpacing(6)
113126
lay.addWidget(
114127
kit.row(self.title, self.hint, self.rage, self.fit, self.spread, stretch=True)
@@ -136,6 +149,11 @@ def _natural_rage(self, on: bool) -> None:
136149
"natural rage", lambda: self.ws.edit("", lambda: graph.set_natural_rage(doc, on))
137150
)()
138151

152+
def _scoped(self, key: str) -> None:
153+
"""A row of the outline picked: that trigger's rules, framed to fit."""
154+
self.ws.scope = key
155+
self.sync()
156+
139157
# what the bar tells: the same intents as the canvas's, one undo step each
140158

141159
def _param(self, node: str, name: str, value: object) -> None:
@@ -156,11 +174,34 @@ def _collect(self, fn: Callable[[graph.Edits], str]) -> None:
156174
self._gesture = graph.Gesture(doc)
157175
self._gesture.run(fn)
158176

177+
def _scope_all(self) -> bool:
178+
return self.ws.scope == ALL
179+
180+
def _wire(self, edit: Callable[..., str], *wire: str) -> Callable[[graph.Edits], str]:
181+
"""The edit of a wire intent, or the refusal a trigger's rows give for it."""
182+
return (lambda e: edit(e, *wire)) if self._scope_all() else self._not_here
183+
184+
@staticmethod
185+
def _not_here(_: graph.Edits) -> str:
186+
raise ManifestError(SCOPED)
187+
188+
def _delete(self, ids: list[str]) -> None:
189+
nodes = list(dict.fromkeys(rows.node_of(i) for i in ids)) # a reference is its move
190+
self._collect(lambda e: graph.delete(e, nodes))
191+
192+
def _moved(self, node: str, x: float, y: float) -> None:
193+
if self._scope_all(): # the rows are laid out for display: a drag snaps back
194+
self._dropped[node] = (x, y)
195+
159196
def _create(self, kind_: str, x: float, y: float) -> None:
160197
doc = self.ws.doc
161-
if doc is not None:
162-
self._made = model.new_id(doc.manifest.behaviour)
163-
self._collect(lambda e: graph.add_block(e, kind_, (x, y)))
198+
if doc is None:
199+
return
200+
if not self._scope_all():
201+
self._collect(self._not_here)
202+
return
203+
self._made = model.new_id(doc.manifest.behaviour)
204+
self._collect(lambda e: graph.add_block(e, kind_, (x, y)))
164205

165206
def _settle(self) -> None:
166207
"""The gesture is whole: one undo step, or one refusal."""
@@ -188,7 +229,7 @@ def edit() -> None:
188229
def _picked(self, node: object) -> None:
189230
"""A move node picks its move, as the Moves table does; the bar shows the node."""
190231
self._show_bar()
191-
name = graph.move_name(node) if isinstance(node, str) else None
232+
name = graph.move_name(rows.node_of(node)) if isinstance(node, str) else None
192233
if name is None or name == self.ws.move:
193234
return
194235

@@ -207,21 +248,39 @@ def sync(self) -> None:
207248
return
208249
if theme.current() is not self._theme:
209250
self._theme = theme.current()
210-
self.view.set_roles(roles())
251+
self.view.set_roles(ROLES)
211252
self.view.set_canvas(theme.current().view)
253+
self._outlined = None
212254
if self._drawn is not m:
213255
self._drawn = m
214-
self._draw(m)
256+
self._read = graph.read(m)
257+
self._triggers = model.triggers(m)
258+
self._scope = None
215259
kit.put(self.rage, graph.natural_rage(m))
216-
self._follow(m)
260+
block = self.ws.take_block()
261+
if block in m.behaviour.blocks:
262+
self._reach(block)
263+
if self.ws.scope not in (ALL, *(t.key for t in self._triggers)):
264+
self.ws.scope = ALL # its trigger is gone
265+
if self._scope != self.ws.scope or self._outlined is None:
266+
self._draw(m)
267+
self._follow(m, block)
217268
self._show_bar()
218269

270+
def _reach(self, block: str) -> None:
271+
"""A block that a finding named and the scope does not draw: its trigger's rows, or All
272+
for a block on no rule."""
273+
scope = next((t for t in self._triggers if t.key == self.ws.scope), None)
274+
if scope is not None and not any(block in p.blocks for _, p in scope.rules):
275+
self.ws.scope = triggers.owner(self._triggers, block) or ALL
276+
219277
def _show_bar(self) -> None:
220278
"""The bar shows the one node selected, from the document: a refused edit goes back."""
221279
m, ids = self.ws.manifest, self.view.selected()
222280
self.bar.show_detail(self._detail(m, ids[0]) if m is not None and len(ids) == 1 else None)
223281

224282
def _detail(self, m: Manifest, node: str) -> Detail | None:
283+
node = rows.node_of(node)
225284
name = graph.move_name(node)
226285
if name is not None:
227286
mv = m.moves.get(name)
@@ -252,13 +311,52 @@ def _move_tip(mv: Move) -> str:
252311
return "\n".join([f"{kind(mv)} · {plays}", *([f"Note: {mv.label}"] if mv.label else [])])
253312

254313
def _draw(self, m: Manifest) -> None:
255-
r = self._read = graph.read(m)
256-
nodes = self._blocks(m, r) + self._moves(m)
257-
self.view.show(nodes, [LinkSpec(*w) for w in graph.wires(m)])
258-
loose = f" · {len(r.loose)} loose" if r.loose else ""
259-
self.title.setText(
260-
f"{len(m.behaviour.blocks)} blocks · {len(r.paths)} of {model.SEAM_RULES} paths" + loose
314+
r = self._read
315+
assert r is not None
316+
scope = self.ws.scope
317+
total = len(r.paths)
318+
key = (tuple((t.key, t.ranks) for t in self._triggers), total, scope, theme.current())
319+
if key != self._outlined:
320+
self._outlined = key
321+
self.outline.set_triggers(self._triggers, total, scope)
322+
blocks, moves = self._blocks(m, r), self._moves(m)
323+
t = next((t for t in self._triggers if t.key == scope), None)
324+
if t is None:
325+
self.view.show(blocks + moves, [LinkSpec(*w) for w in graph.wires(m)])
326+
loose = f" · {len(r.loose)} loose" if r.loose else ""
327+
self.title.setText(
328+
f"{len(m.behaviour.blocks)} blocks · {total} of {model.SEAM_RULES} paths" + loose
329+
)
330+
else:
331+
self._draw_rows(m, r, t, blocks, moves)
332+
self.title.setText(triggers.says(t, total))
333+
self.hint.setText(HINT if t is None else SCOPED)
334+
self.spread.setEnabled(t is None)
335+
if self._scope is not None and self._scope != scope:
336+
self.view.fit()
337+
self._scope = scope
338+
339+
def _draw_rows(
340+
self,
341+
m: Manifest,
342+
r: graph.Reading,
343+
t: Trigger,
344+
blocks: list[NodeSpec],
345+
moves: list[NodeSpec],
346+
) -> None:
347+
"""`t`'s rules as rows: drawn once to measure the nodes, then placed."""
348+
notes = {i: b.label for i, b in m.behaviour.blocks.items()}
349+
plan = rows.plan(
350+
t,
351+
{n.id: n for n in blocks},
352+
notes,
353+
dict(zip(m.moves, moves, strict=True)),
354+
rows.players(r.paths),
261355
)
356+
links = list(plan.links)
357+
self.view.show([dataclasses.replace(n, at=(0.0, 0.0)) for n in plan.nodes], links)
358+
placed, frames = rows.place(plan, self.view.sizes())
359+
self.view.show(placed, links, frames)
262360

263361
@staticmethod
264362
def _blocks(m: Manifest, r: graph.Reading) -> list[NodeSpec]:
@@ -302,10 +400,9 @@ def _moves(m: Manifest) -> list[NodeSpec]:
302400
)
303401
return out
304402

305-
def _follow(self, m: Manifest) -> None:
403+
def _follow(self, m: Manifest, block: str | None) -> None:
306404
"""A block a finding named is picked and framed; a move picked elsewhere is selected."""
307405
ws = self.ws
308-
block = ws.take_block()
309406
if block in m.behaviour.blocks:
310407
self.view.select([block])
311408
self.view.frame([block])

0 commit comments

Comments
 (0)