Commit 42510e7
Let dragLimit and snap grids describe their area with inequalities, not just a rectangle
* Let dropLimit be a dynamic expression and show what it amounts to
A drop limit had to be a fixed number, so a game whose limit depends on
the state (the number of seats taken, a difficulty setting, a counter)
had to have a routine write the number into every widget that uses it.
dropLimit now also takes an expression - "${PROPERTY seats OF board}",
"${PROPERTY level} * 2", "min ${PROPERTY a} 5" - written the way a
routine writes one and using the same operations (compute.js). It is
evaluated wherever the limit is enforced, so it follows the state by
itself, and a pile with showLimit shows what it currently amounts to,
redrawn when a property the expression reads changes anywhere.
An expression that does not evaluate to a number (a typo, a widget that
is gone) reads as no limit, so it can never make a widget refuse
everything.
The JSON editor gets a "dynamic limit"/"fixed limit" button on the
dropLimit line, and the validator accepts the new shape. The curated
"Drop limit" number input cannot show an expression, so a widget that
has one keeps dropLimit in the generic "Other properties" list until
the properties sidebar can edit expressions - a design that is up for
discussion in the PR.
Co-Authored-By: Claude <noreply@anthropic.com>
* Revert "Let dropLimit be a dynamic expression and show what it amounts to"
This reverts commit 7abc623. The request behind this PR was about
dragLimit - the area a widget can be dragged in - and about describing
that area with an inequality like "2x^2+y>4", not about the limit on
how many widgets can be dragged into a holder. The dropLimit take is
kept in the branch history in case anyone wants it as its own PR.
Co-Authored-By: Claude <noreply@anthropic.com>
* Let dragLimit describe the drag area with inequalities like "2x^2+y>4"
dragLimit could only be a rectangle of four fixed numbers, so a piece
that has to stay on a board of any other shape - a disc, a triangle, a
track, anything curved - could not be limited at all, and a rectangle
that depends on the state had to be written by a routine.
Each side (minX/maxX/minY/maxY) now also takes an expression, and a new
"condition" - one inequality or a list of them, written in x and y -
bounds the drag to any area that can be written down:
"dragLimit": { "condition": "2x^2 + y > 4" }
"dragLimit": { "condition": [ "y > x - 500", "y < 620" ] }
"dragLimit": { "minX": 0, "maxX": "${PROPERTY width OF board} - 100" }
client/js/expression.js is the small language behind both: ordinary
infix maths (+ - * / % ^, brackets, comparisons, && || !, the usual
functions), where x and y are the position being tested, any other name
is a property of the widget, and ${PROPERTY name OF id} reads another
widget's. A number in front of a name or a bracket multiplies, so
"2x^2+y>4" means what it looks like. Nothing in it touches the room
state or the DOM, so it is evaluated on every mouse move safely.
A refused position falls back to moving on one axis only, so a widget
slides along the edge of its area rather than sticking to it. A widget
that is outside its area is not held in place, and an expression that
cannot be read limits nothing, so neither can leave a piece stuck.
The JSON editor gets a "condition" button on dragLimit, the validator
knows the new shape, and the properties sidebar leaves a dragLimit that
uses an expression or a condition to "Other properties" - its four
number inputs cannot show one. Drawing the area in the editor is a
design question and is proposed in the PR rather than invented here.
Co-Authored-By: Claude <noreply@anthropic.com>
* Keep a dragLimit editable when it turns into an expression mid-edit
The curated "Drag limits" rectangle steps aside for a dragLimit that
uses an expression or a condition, which the four number inputs cannot
show. That decision was only made while the panel was built, so writing
a condition in the JSON editor with the panel open left the inputs
standing (empty, and overwriting the condition with the next keystroke)
and left the property out of "Other properties", where it should appear
instead. Both now follow the property.
Co-Authored-By: Claude <noreply@anthropic.com>
* Edit dragLimit conditions in the sidebar and reject expressions that lie
Addresses the review on #3104. The "Drag limits" section no longer steps
aside for an expression or a condition: each of the four sides takes a
number or an expression, and a "Condition" field below them holds one
inequality per line, so the whole area is editable without the JSON editor.
The parser is stricter and cheaper where it was quietly wrong:
- "0 < x < 500" read as "(0 < x) < 500", i.e. true wherever the widget is.
Comparisons are no longer chainable and say so instead.
- Implicit multiplication is only ever a number in front of a name or a
bracket, so the stray space in "2 3x" is reported rather than meaning 6x.
- && and || evaluate their right side only when it decides the result, so a
condition can guard against the widget it reads being gone.
- The validator parses every side and condition, so a typo shows up in edit
mode instead of turning into a limit that limits nothing.
- Tokens are cached per expression instead of being recomputed on every
mouse move, and the function/constant tables no longer answer for
inherited keys like "constructor".
Also: the fallback a refused condition falls back to is put through the
rectangle, so a widget outside its sides can never be handed back there, and
a "condition" of null reads as no condition.
* Keep the drag limit sides on one line and report a bad expression where it is typed
Addresses the UI review on #3104. The four sides stretched to fit an
expression whether they held one or not, so a plain rectangle wrapped into
four ragged rows at laptop width. Only a side actually holding an expression
takes a line of its own now, and each side carries its axis in its own label
("X min"/"X max") instead of a shared "X from/to" that a wrapped row leaves
behind.
A mistyped expression or condition was only reported in the Debug module's
validation table, i.e. not where it was typed and not where it does nothing:
property inputs take a "validate" option that puts the problem under the
input, and both the sides and the conditions use it. The validator collects
every problem of a dragLimit instead of returning at the first one.
Smaller things from the same review:
- an expression that does not fit its field is readable as a tooltip
- arrow keys step a number again, which type=text had dropped
- the conditions field is three lines tall, labelled "Conditions (one per
line)", and says below it what x and y are and that every line has to hold
- the section help text describes a condition instead of only the rectangle,
the hint says "2x means 2 * x" instead of naming the parser rule, and the
toggle hint says that switching it off drops the conditions too
- the section summary counts the conditions, since a condition-only limit
otherwise looks unset next to four empty sides
- the JSON editor buttons read "add condition" / "add another condition"
Co-Authored-By: Claude <noreply@anthropic.com>
* Show the area a widget can be dragged in on the board while editing
Asked for on Discord: "there should be a button on the editor that shows
the drag limit area of a widget while on editor mode". The four sides and
the conditions of a dragLimit describe an area that cannot be read off the
numbers, so the "Drag limits" section gained a "Show on board" switch - the
same control and wording the "Snap grid" section uses for its lattice - that
shades every position the widget's top left corner can be dragged to.
The shading is drawn into the widget's parent, i.e. the coordinate space the
limit is measured in, so a piece inside a holder gets its area in the
holder's coordinates and clipped to it. It appears with the limit, follows
every edit of it, and goes away when the switch is off, the section is
collapsed, another widget is selected or edit mode is left. Nothing is ever
drawn in play.
An area a condition describes is not computed but sampled: ~10000 points,
each asking the widget itself whether a drag may end there. dragLimitAllows()
is that question, and the part of dragLimitedCoord() that reads the limit
moved into dragLimitRules() so both use the same rules - dragLimitedCoord()
behaves exactly as before. The drawing waits for a pause in typing, and reads
the four sides once for the whole canvas unless one of them is an expression
that actually varies with the position.
Co-Authored-By: Claude <noreply@anthropic.com>
* Let a dragLimit carry a piece to the edge of its area and along it
A drag that a dragLimit condition refused fell back to moving on one axis
only, at the exact position the mouse reported. Against anything that is
not an axis-parallel line that stops the piece early and in steps: it came
to rest wherever the last mouse move before the boundary happened to fall
- up to a whole mouse step short of a limit that looked much wider than
the piece could reach - and once there it could only stay put or move
along one axis, so an inclined edge made it move in a staircase and a
curve caught it wherever the tangent was horizontal or vertical.
The dragged position is now put where the area comes closest to the
pointer instead: the movement asked for is turned away from the pointer
by the smallest angle that lands it in the area, which is what sliding
along an edge is, and repeating that follows an edge that curves away.
Both the straight movement and the turned one are found by bisection, so
the piece rests on the boundary itself whatever the mouse did in between,
and the whole position it is finally given is the one of the four around
that point that is nearest the pointer and still inside.
Measured in a browser, with the pointer circling 500 units outside a
limit of "|p - centre| < 300": the piece used to hop between four
positions between 36 and 297 units from the centre, standing still for
several mouse moves at a time, and now stays between 299.3 and 300.0 all
the way round. Dragged into "y > x + 100" it used to stop dead at the
edge and now follows it.
* Tell a widget on the edge of its drag area from one outside it
A refused drag looked for a position to start its search from by mixing one
coordinate of the pointer with one of the widget, which says nothing about
where the widget is: a widget outside a disc could be pulled onto it because
the mixture happened to land in the middle, and a widget on a strict vertical
edge was let go entirely because no mixture was inside. It now asks the whole
positions around the widget instead, which distinguishes sitting on the
boundary from being away from the area.
The editor's area preview also read a side once for the whole drawing whenever
it evaluated the same at two opposite corners of the parent, which "(x - 800)^2"
does while differing everywhere in between, and it only redrew for a fixed list
of properties. Both now go by the names an expression actually reads, for which
expression.js gained expressionNames().
* Run the drag limit tests in CI and judge a dragLimit side where it applies
Four things the fourth review of #3104 found:
- tests/testcafe/draglimit.js was in neither TestCafe workflow matrix, so the
three drags it makes never ran outside a local checkout.
- The validator refused a side written as null although the engine reads it as
"no limit on that side" - a false positive in edit mode on exactly the games
the null -> 0 change affects. That change of meaning now also carries a
FileUpdater step (v22), which writes down the 0 those saves always clamped to.
- A side that reads x or y ("maxX": "y") was read once where the pointer is and
then used to judge every candidate position, so a drag could end up outside
the area the editor's drawing shades. Both now ask the widget the same
question - dragLimitRules().varies - and re-read the sides per position.
- The parser refused "0 < x < 500" but not "(0 < x) < 500", which is the same
limit that limits nothing, and read "2^2x" as (2^2)*x where it is written to
mean 2^(2x); both are now reported. The drawing is a canvas inside the parent
widget, which drops it whenever it renders its own content, so every property
of the parent redraws it.
* Give dragLimit expressions the same functions and constants as var
sin/cos/tan were radians in a dragLimit expression and degrees in a
routine, "pi" was lowercase where var spells it PI, and asin/acos/atan/
cbrt/log10/log2/trunc were missing entirely - so a formula moved between
the two languages could quietly compute something else. The expression
language now has var's full numeric table, with var's meaning, plus ===
and !== so an expression copied out of a routine still parses.
Left out of it are random/randInt/randRange: an area that is not the same
twice can neither be slid along nor drawn, and everything that is about
strings, arrays or colours rather than numbers.
A new test holds every function and constant against the operation of the
same name in compute_ops, so the two cannot drift apart again.
* Add a "Properties - Drag Limits" tutorial with a rectangle and a shapes variant
Two variants of a new tutorial in the public library. "Rectangle" is the
four sides: one piece limited to a plain minX/maxX/minY/maxY - so its
corner reaches the frame and the piece hangs out of it - and one whose
sides are expressions read off the frame widget, with a button that
resizes that frame so the limit visibly follows the state.
"Any shape" is the condition: five buttons write a different dragLimit
onto one piece and shade the area it describes - the same rectangle
written as four conditions, a disc around the middle of the room, a
regular hexagon that mixes minX/maxX with two abs() conditions, a box
with a box cut out of it, and no limit at all. Each button also shows the
JSON it just wrote and puts the piece back inside the new area.
* Update tutorial library image and some of the text
* Fix tutorial title
* Give dragLimit an alignX/alignY reference point and keep refused drags on the mouse path
alignX/alignY say which point of the widget the area holds, as the same
fractions of its box a snap grid aligns to: 0.5/0.5 keeps its middle in the
area, so a circle is written around where the middle can go instead of adding
half the widget to x and y in every condition. They are edited with the 3x3
picker the grid uses ("Limited point"), and there is an "add alignX" button on
dragLimit in the JSON editor.
A widget property in a dragLimit expression is now read only where it is
written the way routines write one - ${PROPERTY name}, ${PROPERTY name OF id}.
A bare word other than x and y is a mistake the engine would read as nothing,
so the validator and the sidebar report it as it is typed.
A drag that is refused can now only reach positions it can get to without
leaving the area on the way: the movement is walked rather than halved, so a
widget limited to a ring or a track around the board follows the mouse round
it instead of appearing on the far side of the hole the pointer crossed.
The Drag limits section of the properties sidebar says all of that in its info
hovers rather than in paragraphs above the fields.
Tutorial: a new "Reference point" variant shows the same rectangle holding the
corner, the middle and the bottom right corner of a piece; the "Any shape"
variant is rewritten with alignX/alignY, so its conditions are the shapes
themselves.
Co-Authored-By: Claude <noreply@anthropic.com>
* Update Shell Game with dragLimit for scoreboard
* Report a dragLimit condition that is no condition, and an area nothing satisfies
Closes the last member of the "a limit that silently limits nothing must be
reported" family that this branch already catches for chained comparisons and
for a bare property name: a condition written as maths rather than as an
inequality. "condition": "x - 100" is a number, and a condition reads a number
as true wherever it is not 0, so it used to pass the validator and the sidebar
and then allow every position but the single line x == 100.
expressionError() takes a requireCondition flag - whether an expression answers
with a number or with true/false is decided by how it is written, not by what
anything resolves to, so it can be judged once. The validator's condition branch
and the sidebar's Conditions field both ask for it; the four sides keep the
number-shaped check.
An area that no position satisfies at all (two conditions that contradict each
other, a property that turns out to be 0) is not an error in any single line,
but it still limits nothing, because a widget outside its area is always let go
so that it can never be stuck. The sampling behind the "Show on board" preview
already knows: it now runs whenever the section is open, whether the area is
drawn or not, and says so under the fields when it finds no position.
Also:
- the validator accepts "alignX": "0.5", which the engine reads with + and so
has always honoured, instead of reporting a game that works;
- the tutorial's "Other shapes" variant is laid out in top-down bands like its
two siblings - title, overview, the five buttons as one row, the shape below
them, the JSON and the note under that - rather than in the left/right
text-vs-demo split docs/tutorials.md rules out;
- the tutorial directory is renamed to "Properties - dragLimit" so it matches
its own _meta.info.name and the "Properties - inheritFrom" spelling.
Co-Authored-By: Claude <noreply@anthropic.com>
* Start a drag from where the widget is, not from its holder's coordinates
A widget dragged out of a holder used to be thrown to the far side of a
dragLimit area on the very first mouse move: reported on the Shell Game's score
track, where a marker sitting in the holder for 47 (bottom right) jumped to the
other arm of the track as soon as it was nudged.
moveStart() drops the widget's parent, and nothing converts x and y out of that
parent's coordinates, because the first thing a drag does is overwrite them with
the position it computed from the mouse. That was harmless until dragLimit began
walking a drag from where the widget is, so that it follows the pointer around a
hole instead of jumping over it: for that one move, "where the widget is" read
as the holder's own "2, 2", which for the Shell Game's ring is the top left of
the board - inside the area as well, so the walk started over there.
moveStart() now writes down where the widget is in the coordinates of the room,
taken while its parent still says what x and y mean, and dragLimitedCoord()
walks from that. The first position the drag writes clears it again, and so does
moveEnd, since a click is a moveStart and a moveEnd with no move between them.
Measured on library/games/Shell Game/0.json with the marker in scoreHolder47
(middle at 1537,769), nudged 3 px right: it went to 699,188 and was dropped in
scoreHolder2, and now moves to 1539,768 and is dropped back in scoreHolder47.
Co-Authored-By: Claude <noreply@anthropic.com>
* Let a snap grid apply only where a condition holds, not just in a rectangle
A grid entry could be limited to the rectangle minX/maxX/minY/maxY; it now
also takes a "condition" - one inequality in x and y (the position being
dropped) or a list of them, in the same expression language a dragLimit
condition is written in. Outside the area the grid is not one of the grids
tried, so the widget snaps to whichever other grid covers the position and
to nothing at all where none does, exactly as the rectangle already behaves.
"grid": [ { "x": 100, "y": 100, "condition": "(x - 800)^2 + (y - 500)^2 < 300^2" } ]
The properties sidebar edits it in a "Conditions (one per line)" field right
under the X/Y from/to rows of "Only in part of the parent", reporting a line
that does not parse under the field it was typed into; the area the
conditions describe is traced onto the board in the same dashed line the
rectangle is outlined with, and follows a property it reads while it is
edited. The JSON editor offers "add condition" / "add another condition" on
a grid entry, and the validator parses the expressions in one.
The test room is a new variant of the Properties - Grid tutorial
("Condition Limit", 5.json): a round area, two grids split by a diagonal,
and an area that follows a property while the game is played.
Co-Authored-By: Claude <noreply@anthropic.com>
* Snap onto a grid point inside a condition's area, not the one past its edge
A grid entry limited by a condition was only asked about the position the
widget was dropped at, and then snapped to the lattice point nearest that
position wherever that lay. That point is up to half a cell away in each
direction, and a boundary that runs wherever it likes is easily in between:
dropping a piece well inside a round area could leave it sitting just
outside the very area the grid is limited to.
snapToGrid() now walks the lattice outwards from that point instead and
lands the widget on the nearest one where the grid actually applies - inside
its rectangle and inside the area its conditions describe - ranked by how
far the widget itself moves rather than by the point alignX/alignY aligns.
Where the area holds no lattice point at all (an area narrower than the grid
step) the grid does not apply and the widget stays where it was let go, as
it already does outside the area.
A grid without a condition keeps snapping to the nearest lattice point
wherever that is: that is what it has always done and what games are built
on, so nothing existing changes.
The Condition Limit variant of the Properties - Grid tutorial and the
Conditions hint in the sidebar say where the widget lands.
Co-Authored-By: Claude <noreply@anthropic.com>
* Mark only the snap points a grid's condition holds at, not the whole rectangle
The editor's snap-grid preview draws a dot at every position the widget can
land on. It draws them as one repeating background across the grid's whole
rectangle, which is every lattice point of it - so a grid limited to an area
by a condition was still marked all over the rectangle, promising snap points
on the far side of a boundary the widget can never be put past.
The dots of a grid that carries a condition are drawn one by one instead,
only where the grid applies (the same question snapToGrid asks of a point it
weighs up), while the faint lattice lines stay as the mesh the area is read
against. Each dot is a zero-length subpath with a round cap, so all of them
are one path, and it is the same dot the background paints: the lattice's
color inside a white ring, at the size it has at every zoom.
A lattice with more points than a condition can be read at while it is being
typed (4000) keeps the plain background, where the dots are barely apart
anyway.
Co-Authored-By: Claude <noreply@anthropic.com>
* Tweaks to tutorials
---------
Co-authored-by: VTT AI Agent <300563116+VTT-AI-Agent@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: 96LawDawg <76912527+96LawDawg@users.noreply.github.com>1 parent 3a874a2 commit 42510e7
31 files changed
Lines changed: 4238 additions & 94 deletions
File tree
- .github/workflows
- client
- css/editor
- js
- editor
- sidebar
- widgets
- library
- games/Shell Game
- tutorials
- Properties - Grid
- Properties - dragLimit
- assets
- server
- tests
- client
- server
- testcafe
- validator
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
173 | 173 | | |
174 | 174 | | |
175 | 175 | | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
176 | 179 | | |
177 | 180 | | |
178 | 181 | | |
| |||
186 | 189 | | |
187 | 190 | | |
188 | 191 | | |
189 | | - | |
| 192 | + | |
190 | 193 | | |
191 | 194 | | |
192 | 195 | | |
| |||
195 | 198 | | |
196 | 199 | | |
197 | 200 | | |
198 | | - | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
199 | 279 | | |
200 | 280 | | |
201 | 281 | | |
202 | 282 | | |
203 | | - | |
| 283 | + | |
| 284 | + | |
204 | 285 | | |
205 | 286 | | |
206 | 287 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1914 | 1914 | | |
1915 | 1915 | | |
1916 | 1916 | | |
| 1917 | + | |
| 1918 | + | |
| 1919 | + | |
| 1920 | + | |
| 1921 | + | |
| 1922 | + | |
| 1923 | + | |
| 1924 | + | |
| 1925 | + | |
| 1926 | + | |
| 1927 | + | |
| 1928 | + | |
| 1929 | + | |
| 1930 | + | |
| 1931 | + | |
| 1932 | + | |
| 1933 | + | |
| 1934 | + | |
| 1935 | + | |
| 1936 | + | |
| 1937 | + | |
| 1938 | + | |
| 1939 | + | |
| 1940 | + | |
| 1941 | + | |
| 1942 | + | |
| 1943 | + | |
| 1944 | + | |
| 1945 | + | |
| 1946 | + | |
| 1947 | + | |
| 1948 | + | |
| 1949 | + | |
| 1950 | + | |
| 1951 | + | |
| 1952 | + | |
| 1953 | + | |
| 1954 | + | |
| 1955 | + | |
1917 | 1956 | | |
1918 | 1957 | | |
1919 | 1958 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
602 | 602 | | |
603 | 603 | | |
604 | 604 | | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
605 | 609 | | |
606 | | - | |
| 610 | + | |
607 | 611 | | |
608 | 612 | | |
609 | 613 | | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
610 | 623 | | |
611 | 624 | | |
612 | 625 | | |
| |||
627 | 640 | | |
628 | 641 | | |
629 | 642 | | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
630 | 647 | | |
631 | 648 | | |
632 | 649 | | |
| |||
719 | 736 | | |
720 | 737 | | |
721 | 738 | | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
722 | 752 | | |
723 | 753 | | |
724 | 754 | | |
| |||
729 | 759 | | |
730 | 760 | | |
731 | 761 | | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
732 | 768 | | |
733 | 769 | | |
734 | 770 | | |
| |||
0 commit comments