@cssints/apg-specs 0.0.0-stage → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/apg/accordion.bridge.d.mts +35 -0
- package/dist/apg/accordion.bridge.mjs +326 -0
- package/dist/apg/accordion.model.json +8397 -0
- package/dist/apg/alert.bridge.d.mts +22 -0
- package/dist/apg/alert.bridge.mjs +86 -0
- package/dist/apg/alert.model.json +871 -0
- package/dist/apg/alertdialog.bridge.d.mts +21 -0
- package/dist/apg/alertdialog.bridge.mjs +221 -0
- package/dist/apg/alertdialog.model.json +5232 -0
- package/dist/apg/breadcrumb.bridge.d.mts +26 -0
- package/dist/apg/breadcrumb.bridge.mjs +120 -0
- package/dist/apg/breadcrumb.model.json +574 -0
- package/dist/apg/button.bridge.d.mts +35 -0
- package/dist/apg/button.bridge.mjs +208 -0
- package/dist/apg/button.model.json +5814 -0
- package/dist/apg/carousel.bridge.d.mts +44 -0
- package/dist/apg/carousel.bridge.mjs +262 -0
- package/dist/apg/carousel.model.json +8963 -0
- package/dist/apg/checkbox-group.bridge.d.mts +25 -0
- package/dist/apg/checkbox-group.bridge.mjs +276 -0
- package/dist/apg/checkbox-group.model.json +3863 -0
- package/dist/apg/checkbox.bridge.d.mts +34 -0
- package/dist/apg/checkbox.bridge.mjs +253 -0
- package/dist/apg/checkbox.model.json +3158 -0
- package/dist/apg/combobox-autocomplete.bridge.d.mts +14 -0
- package/dist/apg/combobox-autocomplete.bridge.mjs +326 -0
- package/dist/apg/combobox-autocomplete.model.json +11586 -0
- package/dist/apg/combobox-dialog.bridge.d.mts +17 -0
- package/dist/apg/combobox-dialog.bridge.mjs +250 -0
- package/dist/apg/combobox-dialog.model.json +8981 -0
- package/dist/apg/combobox-grid.bridge.d.mts +17 -0
- package/dist/apg/combobox-grid.bridge.mjs +227 -0
- package/dist/apg/combobox-grid.model.json +8554 -0
- package/dist/apg/combobox-select-only.bridge.d.mts +24 -0
- package/dist/apg/combobox-select-only.bridge.mjs +304 -0
- package/dist/apg/combobox-select-only.model.json +7846 -0
- package/dist/apg/combobox-tree.bridge.d.mts +17 -0
- package/dist/apg/combobox-tree.bridge.mjs +211 -0
- package/dist/apg/combobox-tree.model.json +9345 -0
- package/dist/apg/dialog-modal.bridge.d.mts +35 -0
- package/dist/apg/dialog-modal.bridge.mjs +186 -0
- package/dist/apg/dialog-modal.model.json +7580 -0
- package/dist/apg/disclosure.bridge.d.mts +25 -0
- package/dist/apg/disclosure.bridge.mjs +202 -0
- package/dist/apg/disclosure.model.json +4703 -0
- package/dist/apg/feed.bridge.d.mts +31 -0
- package/dist/apg/feed.bridge.mjs +246 -0
- package/dist/apg/feed.model.json +4523 -0
- package/dist/apg/grid-selection.bridge.d.mts +13 -0
- package/dist/apg/grid-selection.bridge.mjs +160 -0
- package/dist/apg/grid-selection.model.json +6974 -0
- package/dist/apg/grid-sort.bridge.d.mts +12 -0
- package/dist/apg/grid-sort.bridge.mjs +138 -0
- package/dist/apg/grid-sort.model.json +3799 -0
- package/dist/apg/grid.bridge.d.mts +54 -0
- package/dist/apg/grid.bridge.mjs +441 -0
- package/dist/apg/grid.model.json +3802 -0
- package/dist/apg/link.bridge.d.mts +24 -0
- package/dist/apg/link.bridge.mjs +126 -0
- package/dist/apg/link.model.json +694 -0
- package/dist/apg/listbox-multi.bridge.d.mts +10 -0
- package/dist/apg/listbox-multi.bridge.mjs +261 -0
- package/dist/apg/listbox-multi.model.json +10893 -0
- package/dist/apg/listbox-rearrangeable.bridge.d.mts +20 -0
- package/dist/apg/listbox-rearrangeable.bridge.mjs +237 -0
- package/dist/apg/listbox-rearrangeable.model.json +17592 -0
- package/dist/apg/listbox-shared.d.mts +58 -0
- package/dist/apg/listbox-shared.mjs +171 -0
- package/dist/apg/listbox-single.bridge.d.mts +10 -0
- package/dist/apg/listbox-single.bridge.mjs +196 -0
- package/dist/apg/listbox-single.model.json +8024 -0
- package/dist/apg/listbox-typeahead.bridge.d.mts +10 -0
- package/dist/apg/listbox-typeahead.bridge.mjs +153 -0
- package/dist/apg/listbox-typeahead.model.json +5615 -0
- package/dist/apg/menu-button.bridge.d.mts +29 -0
- package/dist/apg/menu-button.bridge.mjs +259 -0
- package/dist/apg/menu-button.model.json +5706 -0
- package/dist/apg/menu-checkable.bridge.d.mts +25 -0
- package/dist/apg/menu-checkable.bridge.mjs +226 -0
- package/dist/apg/menu-checkable.model.json +4277 -0
- package/dist/apg/menu-menubar.bridge.d.mts +27 -0
- package/dist/apg/menu-menubar.bridge.mjs +273 -0
- package/dist/apg/menu-menubar.model.json +9540 -0
- package/dist/apg/meter.bridge.d.mts +28 -0
- package/dist/apg/meter.bridge.mjs +159 -0
- package/dist/apg/meter.model.json +1185 -0
- package/dist/apg/player.bridge.d.mts +24 -0
- package/dist/apg/player.bridge.mjs +342 -0
- package/dist/apg/player.model.json +11013 -0
- package/dist/apg/radio.bridge.d.mts +35 -0
- package/dist/apg/radio.bridge.mjs +371 -0
- package/dist/apg/radio.model.json +5011 -0
- package/dist/apg/slider-multithumb.bridge.d.mts +34 -0
- package/dist/apg/slider-multithumb.bridge.mjs +365 -0
- package/dist/apg/slider-multithumb.model.json +5774 -0
- package/dist/apg/slider.bridge.d.mts +54 -0
- package/dist/apg/slider.bridge.mjs +359 -0
- package/dist/apg/slider.model.json +4881 -0
- package/dist/apg/spinbutton.bridge.d.mts +41 -0
- package/dist/apg/spinbutton.bridge.mjs +327 -0
- package/dist/apg/spinbutton.model.json +7571 -0
- package/dist/apg/switch.bridge.d.mts +30 -0
- package/dist/apg/switch.bridge.mjs +243 -0
- package/dist/apg/switch.model.json +3159 -0
- package/dist/apg/tabs.bridge.d.mts +41 -0
- package/dist/apg/tabs.bridge.mjs +294 -0
- package/dist/apg/tabs.model.json +7659 -0
- package/dist/apg/timeline.bridge.d.mts +36 -0
- package/dist/apg/timeline.bridge.mjs +265 -0
- package/dist/apg/timeline.model.json +6189 -0
- package/dist/apg/toolbar.bridge.d.mts +25 -0
- package/dist/apg/toolbar.bridge.mjs +307 -0
- package/dist/apg/toolbar.model.json +6616 -0
- package/dist/apg/tooltip.bridge.d.mts +36 -0
- package/dist/apg/tooltip.bridge.mjs +208 -0
- package/dist/apg/tooltip.model.json +8295 -0
- package/dist/apg/treegrid.bridge.d.mts +28 -0
- package/dist/apg/treegrid.bridge.mjs +311 -0
- package/dist/apg/treegrid.model.json +17358 -0
- package/dist/apg/treeview.bridge.d.mts +26 -0
- package/dist/apg/treeview.bridge.mjs +396 -0
- package/dist/apg/treeview.model.json +13745 -0
- package/dist/apg/windowsplitter.bridge.d.mts +36 -0
- package/dist/apg/windowsplitter.bridge.mjs +242 -0
- package/dist/apg/windowsplitter.model.json +6253 -0
- package/dist/canvas/camera.model.json +6568 -0
- package/dist/canvas/canvas.bridge.d.mts +190 -0
- package/dist/canvas/canvas.bridge.mjs +729 -0
- package/dist/canvas/pointer.model.json +17095 -0
- package/dist/scenario.d.mts +233 -0
- package/dist/scenario.mjs +313 -0
- package/package.json +57 -4
- package/scenarios/apg/README.md +221 -0
- package/scenarios/apg/accordion.base-ui.json +1005 -0
- package/scenarios/apg/accordion.react-aria.json +929 -0
- package/scenarios/apg/alert.base-ui.json +81 -0
- package/scenarios/apg/alert.react-aria.json +98 -0
- package/scenarios/apg/alertdialog.base-ui.json +331 -0
- package/scenarios/apg/alertdialog.react-aria.json +225 -0
- package/scenarios/apg/breadcrumb.react-aria.json +312 -0
- package/scenarios/apg/button.ariakit.json +136 -0
- package/scenarios/apg/button.base-ui.json +747 -0
- package/scenarios/apg/button.react-aria.json +443 -0
- package/scenarios/apg/carousel.apg.json +909 -0
- package/scenarios/apg/checkbox.ariakit.json +299 -0
- package/scenarios/apg/checkbox.base-ui.json +1126 -0
- package/scenarios/apg/checkbox.react-aria.json +518 -0
- package/scenarios/apg/combobox.ariakit.json +945 -0
- package/scenarios/apg/combobox.base-ui.json +2444 -0
- package/scenarios/apg/combobox.react-aria.json +845 -0
- package/scenarios/apg/dialog-modal.ariakit.json +1102 -0
- package/scenarios/apg/dialog-modal.base-ui.json +775 -0
- package/scenarios/apg/dialog-modal.react-aria.json +638 -0
- package/scenarios/apg/disclosure.apg.json +349 -0
- package/scenarios/apg/disclosure.ariakit.json +163 -0
- package/scenarios/apg/disclosure.base-ui.json +451 -0
- package/scenarios/apg/disclosure.react-aria.json +643 -0
- package/scenarios/apg/feed.apg.json +442 -0
- package/scenarios/apg/grid.ariakit.json +197 -0
- package/scenarios/apg/grid.base-ui.json +720 -0
- package/scenarios/apg/grid.react-aria.json +969 -0
- package/scenarios/apg/landmarks.ariakit.json +93 -0
- package/scenarios/apg/landmarks.react-aria.json +330 -0
- package/scenarios/apg/link.ariakit.json +83 -0
- package/scenarios/apg/link.base-ui.json +88 -0
- package/scenarios/apg/link.react-aria.json +186 -0
- package/scenarios/apg/listbox.ariakit.json +78 -0
- package/scenarios/apg/listbox.base-ui.json +163 -0
- package/scenarios/apg/listbox.react-aria.json +1118 -0
- package/scenarios/apg/menu-button.ariakit.json +924 -0
- package/scenarios/apg/menu-button.base-ui.json +1155 -0
- package/scenarios/apg/menu-button.react-aria.json +1080 -0
- package/scenarios/apg/menubar.ariakit.json +758 -0
- package/scenarios/apg/menubar.base-ui.json +673 -0
- package/scenarios/apg/meter.base-ui.json +243 -0
- package/scenarios/apg/meter.react-aria.json +201 -0
- package/scenarios/apg/radio.ariakit.json +459 -0
- package/scenarios/apg/radio.base-ui.json +581 -0
- package/scenarios/apg/radio.react-aria.json +576 -0
- package/scenarios/apg/slider-multithumb.base-ui.json +316 -0
- package/scenarios/apg/slider-multithumb.react-aria.json +363 -0
- package/scenarios/apg/slider.base-ui.json +544 -0
- package/scenarios/apg/slider.react-aria.json +456 -0
- package/scenarios/apg/spinbutton.base-ui.json +1688 -0
- package/scenarios/apg/spinbutton.react-aria.json +426 -0
- package/scenarios/apg/switch.base-ui.json +456 -0
- package/scenarios/apg/switch.react-aria.json +303 -0
- package/scenarios/apg/table.ariakit.json +65 -0
- package/scenarios/apg/table.react-aria.json +221 -0
- package/scenarios/apg/tabs.ariakit.json +329 -0
- package/scenarios/apg/tabs.base-ui.json +875 -0
- package/scenarios/apg/tabs.react-aria.json +644 -0
- package/scenarios/apg/toolbar.ariakit.json +595 -0
- package/scenarios/apg/toolbar.base-ui.json +1189 -0
- package/scenarios/apg/toolbar.react-aria.json +383 -0
- package/scenarios/apg/tooltip.ariakit.json +441 -0
- package/scenarios/apg/tooltip.base-ui.json +411 -0
- package/scenarios/apg/tooltip.react-aria.json +454 -0
- package/scenarios/apg/treegrid.apg.json +2558 -0
- package/scenarios/apg/treegrid.react-aria.json +5094 -0
- package/scenarios/apg/treeview.apg.json +928 -0
- package/scenarios/apg/windowsplitter.apg.json +557 -0
- package/scenarios/canvas/README.md +69 -0
- package/scenarios/canvas/camera.contract.json +237 -0
- package/scenarios/canvas/camera.excalidraw.json +57 -0
- package/scenarios/canvas/marquee.excalidraw.json +122 -0
- package/scenarios/canvas/pan.excalidraw.json +46 -0
- package/scenarios/canvas/pointer.contract.json +528 -0
- package/scenarios/parts.json +291 -0
- package/shared/aria.qnt +186 -0
- package/shared/details-group.qnt +50 -0
- package/shared/focusgroup.qnt +152 -0
- package/shared/invokers.qnt +81 -0
- package/shared/popover.qnt +71 -0
- package/specs/apg/STATIC.md +103 -0
- package/specs/apg/accordion.qnt +578 -0
- package/specs/apg/alert.qnt +131 -0
- package/specs/apg/alertdialog.qnt +455 -0
- package/specs/apg/breadcrumb.qnt +88 -0
- package/specs/apg/button.qnt +445 -0
- package/specs/apg/carousel.qnt +732 -0
- package/specs/apg/checkbox-group.qnt +388 -0
- package/specs/apg/checkbox.qnt +394 -0
- package/specs/apg/combobox-autocomplete.qnt +764 -0
- package/specs/apg/combobox-dialog.qnt +627 -0
- package/specs/apg/combobox-grid.qnt +632 -0
- package/specs/apg/combobox-select-only.qnt +626 -0
- package/specs/apg/combobox-tree.qnt +633 -0
- package/specs/apg/dialog-modal.qnt +499 -0
- package/specs/apg/disclosure.qnt +309 -0
- package/specs/apg/feed.qnt +485 -0
- package/specs/apg/grid-selection.qnt +512 -0
- package/specs/apg/grid-sort.qnt +330 -0
- package/specs/apg/grid.qnt +360 -0
- package/specs/apg/link.qnt +119 -0
- package/specs/apg/listbox-multi.qnt +799 -0
- package/specs/apg/listbox-rearrangeable.qnt +801 -0
- package/specs/apg/listbox-single.qnt +511 -0
- package/specs/apg/listbox-typeahead.qnt +396 -0
- package/specs/apg/menu-button.qnt +363 -0
- package/specs/apg/menu-checkable.qnt +304 -0
- package/specs/apg/menu-menubar.qnt +531 -0
- package/specs/apg/meter.qnt +129 -0
- package/specs/apg/player.qnt +1081 -0
- package/specs/apg/radio.qnt +394 -0
- package/specs/apg/slider-multithumb.qnt +441 -0
- package/specs/apg/slider.qnt +396 -0
- package/specs/apg/spinbutton.qnt +494 -0
- package/specs/apg/switch.qnt +389 -0
- package/specs/apg/tabs.qnt +412 -0
- package/specs/apg/timeline.qnt +612 -0
- package/specs/apg/toolbar.qnt +364 -0
- package/specs/apg/tooltip.qnt +553 -0
- package/specs/apg/treegrid.qnt +983 -0
- package/specs/apg/treeview.qnt +1004 -0
- package/specs/apg/windowsplitter.qnt +487 -0
- package/specs/canvas/camera.qnt +407 -0
- package/specs/canvas/pointer.qnt +841 -0
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
// Infinite canvas: the pointer / interaction machine over a fixed node set.
|
|
2
|
+
// Implements docs/research/canvas-prior-art.md (Decision 2, items 4-7). Bean e9jm.
|
|
3
|
+
//
|
|
4
|
+
// STATES: idle | pointing (down, below the drag threshold) | panning | marquee | dragging.
|
|
5
|
+
// idle -> pointing | panning; pointing -> marquee | dragging | idle; panning | marquee |
|
|
6
|
+
// dragging -> idle. `prev` records the previous mode so OnlyLegalTransitions can check the edge.
|
|
7
|
+
//
|
|
8
|
+
// WORLD / SCREEN SPLIT (the contract). Pointer positions and the drag threshold are SCREEN
|
|
9
|
+
// quantities; the grab point, node geometry, marquee rectangle and snap candidates are WORLD
|
|
10
|
+
// quantities. The camera is the camera.qnt convention (world-offset, z = zoom * 4096, t in
|
|
11
|
+
// 1/256 world units, round-half-up division); node coordinates are whole world units, pointer-
|
|
12
|
+
// derived world points are in 1/256 units ("fp"). The ~10 camera defs below are COPIES of
|
|
13
|
+
// camera.qnt's (a spec file cannot import a machine, and shared/ is closed to this ticket):
|
|
14
|
+
// test/canvas/pointer.test.ts asserts they are structurally identical.
|
|
15
|
+
//
|
|
16
|
+
// WHAT IS ENCODED
|
|
17
|
+
// - ONE CAPTURE OWNER: `owner` is the pointer id holding the capture (0 iff idle). While a
|
|
18
|
+
// gesture runs a pointerdown from the other pointer is ignored, as are its moves and ups.
|
|
19
|
+
// - DRAG THRESHOLD in screen px (DRAG_PX = 4, squared distance > 16 leaves `pointing`). The move
|
|
20
|
+
// pool contains positions at 5, 16 (boundary: still pointing) and 25 squared px from the down
|
|
21
|
+
// position, at every zoom.
|
|
22
|
+
// - A DRAG = POINTERDOWN SNAPSHOT + WORLD DELTA: committed node positions do not change during a
|
|
23
|
+
// drag; the live delta (ddx, ddy) is f(world pointer, world grab point) and is recomputed on
|
|
24
|
+
// every move AND on every camera change (zoom_to), never accumulated. The grabbed point stays
|
|
25
|
+
// under the pointer within half a unit (DragPreservesGrabOffset), plus the snap distance when
|
|
26
|
+
// snapped. pointercancel aborts to the snapshot (nothing was committed).
|
|
27
|
+
// - SNAPPING, host policy reduced to a grid (candidates every GRID = 50 units on the dragged node's
|
|
28
|
+
// origin): SNAP_PX = 8 SCREEN px, converted by the zoom (a world distance d snaps iff d * z <=
|
|
29
|
+
// 8 * ZONE). Guides are an OUTPUT cleared on every non-drag state.
|
|
30
|
+
// - MARQUEE, mode a parameter (contain default, as Excalidraw; tldraw touch / xyflow full differ),
|
|
31
|
+
// selection live while the rectangle changes, shift = additive over the pointerdown snapshot.
|
|
32
|
+
// contain is checked to be a subset of overlap (ContainSubsetOfOverlap).
|
|
33
|
+
// - CULLING as a derived predicate: a node is culled iff outside the viewport AND not exempt;
|
|
34
|
+
// selected, cursor (focus candidate) and editing nodes are exempt. Geometry, not the DOM,
|
|
35
|
+
// decides hits and marquee, so a culled node can still be selected.
|
|
36
|
+
// - TOPMOST HIT WINS: higher node index = higher z-order.
|
|
37
|
+
// - HOST-CONTROLLED POSITIONS (host_move): a host (auto-layout, a controlled scene) sets one node's
|
|
38
|
+
// committed position in any mode, clamped into the world bound (NodesInWorldBound). A running
|
|
39
|
+
// drag or marquee is re-solved against the new geometry, like zoom_to for the camera. The one
|
|
40
|
+
// thing it can break is "the target was the topmost hit at pointerdown", so `hostMoved`
|
|
41
|
+
// (cleared when the gesture ends) stands TargetIsTopmostHit down for that gesture. Node SIZES
|
|
42
|
+
// (constants here) and the Z-ORDER (index = z) are host-controlled in the primitive too, but not
|
|
43
|
+
// modelled: tested directly in packages/headless/test/canvas.controlled.browser.test.tsx.
|
|
44
|
+
//
|
|
45
|
+
// ACCESSIBILITY CONTRACT (no focus model is chosen here; h9vk decides). The cursor, the selection
|
|
46
|
+
// and the node geometry live in WORLD space with no DOM reference. `annCount` / `annSel` are
|
|
47
|
+
// OUTPUTS: annCount increases (and annSel carries the new selection) whenever a gesture ends with a
|
|
48
|
+
// changed selection; never mid-gesture. Real DOM focus and virtual focus + live region can both
|
|
49
|
+
// consume them; `cursor` is the node a focus model would move to.
|
|
50
|
+
//
|
|
51
|
+
// INVARIANTS ACHIEVED (the ticket's list): selection is a subset of the nodes (SelectionSubsetOfNodes,
|
|
52
|
+
// IdsInRange); exactly one capture owner (OneCaptureOwner); a gesture is reachable only from idle
|
|
53
|
+
// (OnlyLegalTransitions, via `prev`); drag preserves the grab offset (DragPreservesGrabOffset,
|
|
54
|
+
// DragIsSnapshotPlusDelta); contain selects a subset of overlap (ContainSubsetOfOverlap) and a marquee
|
|
55
|
+
// selects exactly its mode's hits (MarqueeSelectionExact). Also IdleIsClean, GestureTargetConsistent,
|
|
56
|
+
// PointingWithinThreshold, TargetIsTopmostHit, SnappedPositionOnGrid, NoMissedSnap,
|
|
57
|
+
// GuidesOnlyWhileSnapping, PanKeepsGrabUnderPointer, CameraInBound, NodesInWorldBound,
|
|
58
|
+
// MarqueeAdditiveKeepsBase, ExemptNodesNeverCulled, CulledOnlyWhenOffscreen,
|
|
59
|
+
// EditingIsTheSingleSelection, AnnouncementsTrackSelection. The two culling invariants restate the
|
|
60
|
+
// `culled` def and cannot fail on their own; the test mutates the def to show they catch a forgetful
|
|
61
|
+
// implementation. Not achieved: "a node stays within bounds while resizing" (no resize here).
|
|
62
|
+
//
|
|
63
|
+
// NOT MODELLED: host-set sizes and z-order (see HOST-CONTROLLED POSITIONS), resize / rotate
|
|
64
|
+
// handles, edge auto-pan, pinch (two-pointer) zoom, locked / hidden nodes, groups and frames,
|
|
65
|
+
// hollow-shape hit rules, undo, keyboard, wheel normalisation. A touch
|
|
66
|
+
// (coarse) threshold is host policy. The grid is this model's instance of the host's snap
|
|
67
|
+
// candidates: the primitive takes them as an injected per-axis function (whole world units) and
|
|
68
|
+
// defaults to this grid.
|
|
69
|
+
//
|
|
70
|
+
// GENERATOR GAPS HIT: no const / collections (3 nodes = 6 position vars + pure defs; selection =
|
|
71
|
+
// bitmask with arithmetic bit tests, no bitwise ops); one nondet per action (touch = pointer * 12 +
|
|
72
|
+
// palette); no let in defs; every action assigns every var (frame lines emitted by a throwaway
|
|
73
|
+
// script); `size`-like builtin names are rejected (QNT101); a spec cannot import another machine
|
|
74
|
+
// (camera math is duplicated, see above).
|
|
75
|
+
module canvas_pointer {
|
|
76
|
+
var mode: str // "idle" | "pointing" | "panning" | "marquee" | "dragging"
|
|
77
|
+
var owner: int // capture owner: pointer id (1 or 2), 0 = nobody (iff idle)
|
|
78
|
+
var prev: str // the mode before the last event (checks the legal-edge graph)
|
|
79
|
+
var tgt: int // node under the pointer at pointerdown (-1 = empty canvas / pan)
|
|
80
|
+
var shift: bool // additive-select modifier, sampled at pointerdown (frozen while a gesture runs)
|
|
81
|
+
var space: bool // pan modifier (hand tool), sampled at pointerdown
|
|
82
|
+
var mqmode: str // marquee mode parameter: "contain" | "overlap"
|
|
83
|
+
var snapOn: bool // snapping enabled (sampled at pointerdown)
|
|
84
|
+
var tx: int // camera x, world units * TONE (same convention as camera.qnt)
|
|
85
|
+
var ty: int // camera y
|
|
86
|
+
var z: int // camera zoom * ZONE
|
|
87
|
+
var px: int // pointer, screen px
|
|
88
|
+
var py: int
|
|
89
|
+
var downx: int // pointerdown position, screen px (drag threshold origin)
|
|
90
|
+
var downy: int
|
|
91
|
+
var gx: int // WORLD point under the pointer at pointerdown (fp); the grab point of drag / pan / marquee
|
|
92
|
+
var gy: int
|
|
93
|
+
var sel: int // selection: bit i = node i is selected (world-space ids, no DOM)
|
|
94
|
+
var selSnap: int // selection at pointerdown (abort-to-snapshot, additive base); == sel when idle
|
|
95
|
+
var cursor: int // last interacted node, -1 = none (consumed by whichever focus model h9vk picks)
|
|
96
|
+
var editing: int // node being edited, -1 = none
|
|
97
|
+
var n0x: int // COMMITTED node positions, whole world units (sizes are constants)
|
|
98
|
+
var n0y: int
|
|
99
|
+
var n1x: int
|
|
100
|
+
var n1y: int
|
|
101
|
+
var n2x: int
|
|
102
|
+
var n2y: int
|
|
103
|
+
var ddx: int // live drag delta, whole units: f(snapshot, pointer) - recomputed, never accumulated
|
|
104
|
+
var ddy: int
|
|
105
|
+
var guide: int // snap guides, OUTPUT: bit 0 = x snapped, bit 1 = y snapped; 0 unless dragging
|
|
106
|
+
var annCount: int // announcements emitted (OUTPUT: a live region / focus model reads this)
|
|
107
|
+
var annSel: int // selection carried by the last announcement
|
|
108
|
+
var hostMoved: bool // a host_move landed during the running gesture (history: relaxes TargetIsTopmostHit); false when idle
|
|
109
|
+
|
|
110
|
+
// ---- constants (camera constants are copies of camera.qnt) --------------
|
|
111
|
+
def ZONE: int = 4096
|
|
112
|
+
def TONE: int = 256
|
|
113
|
+
def TZ: int = ZONE * TONE
|
|
114
|
+
def ZMIN: int = 256
|
|
115
|
+
def ZMAX: int = 32768
|
|
116
|
+
def WB: int = 1000000
|
|
117
|
+
def WT: int = WB * TONE
|
|
118
|
+
def DRAG_PX: int = 4
|
|
119
|
+
def SNAP_PX: int = 8
|
|
120
|
+
def GRID: int = 50
|
|
121
|
+
def VW: int = 1024
|
|
122
|
+
def VH: int = 768
|
|
123
|
+
|
|
124
|
+
// ---- integer helpers (absI, clampI, fdiv, rdiv are copies of camera.qnt) -
|
|
125
|
+
def absI(a: int): int = if (a < 0) -a else a
|
|
126
|
+
def minI(a: int, b: int): int = if (a < b) a else b
|
|
127
|
+
def maxI(a: int, b: int): int = if (a > b) a else b
|
|
128
|
+
def clampI(v: int, lo: int, hi: int): int = if (v < lo) lo else if (v > hi) hi else v
|
|
129
|
+
def fdiv(a: int, b: int): int = if (a >= 0) a / b else -((-a + b - 1) / b)
|
|
130
|
+
def rdiv(a: int, b: int): int = fdiv(2 * a + b, 2 * b)
|
|
131
|
+
def one(b: bool): int = if (b) 1 else 0
|
|
132
|
+
|
|
133
|
+
// ---- camera (copies of camera.qnt) ---------------------------------------
|
|
134
|
+
def screenToWorld(s: int, t: int, zf: int): int = rdiv(s * TZ, zf) - t
|
|
135
|
+
def clampZ(zf: int): int = clampI(zf, ZMIN, ZMAX)
|
|
136
|
+
def clampT(t: int): int = clampI(t, -WT, WT)
|
|
137
|
+
def anchoredT(t: int, zf: int, zf2: int, s: int): int =
|
|
138
|
+
clampT(t + rdiv(s * TZ, zf2) - rdiv(s * TZ, zf))
|
|
139
|
+
// panning: keep the grab world point g under screen point s
|
|
140
|
+
def panGrab(s: int, zf: int, g: int): int = clampT(rdiv(s * TZ, zf) - g)
|
|
141
|
+
def zoomPreset(k: int): int =
|
|
142
|
+
if (k == 0) ZMIN else if (k == 1) 2048 else if (k == 2) 4096 else if (k == 3) 8192 else ZMAX
|
|
143
|
+
|
|
144
|
+
// ---- the fixed node set: 0 (bottom) .. 2 (top); sizes are constants, positions are vars
|
|
145
|
+
def NW(i: int): int = if (i == 0) 200 else if (i == 1) 200 else 100
|
|
146
|
+
def NH(i: int): int = if (i == 0) 100 else if (i == 1) 150 else 100
|
|
147
|
+
def pick(i: int, a: int, b: int, c: int): int = if (i == 0) a else if (i == 1) b else c
|
|
148
|
+
// node ids as bitmasks (3 bits)
|
|
149
|
+
def pow2(k: int): int = if (k == 0) 1 else if (k == 1) 2 else 4
|
|
150
|
+
def bit(m: int, k: int): bool = (m / pow2(k)) % 2 == 1
|
|
151
|
+
def maskOr(a: int, b: int): int =
|
|
152
|
+
one(or(bit(a, 0), bit(b, 0))) + 2 * one(or(bit(a, 1), bit(b, 1))) + 4 * one(or(bit(a, 2), bit(b, 2)))
|
|
153
|
+
def toggleBit(m: int, k: int): int = if (bit(m, k)) m - pow2(k) else m + pow2(k)
|
|
154
|
+
def single(m: int): int = if (m == 1) 0 else if (m == 2) 1 else if (m == 4) 2 else -1
|
|
155
|
+
|
|
156
|
+
// ---- screen palette (px): 0-3 on node 0, at 0 / 5 / 16 / 25 squared px from position 0;
|
|
157
|
+
// 4 in nodes 0 and 1 (node 1 is on top); 5 node 1; 6 node 2; 7 empty; 8 node 1; 9-11 empty
|
|
158
|
+
def posX(k: int): int =
|
|
159
|
+
if (k == 0) 150 else if (k == 1) 152 else if (k == 2) 154 else if (k == 3) 155
|
|
160
|
+
else if (k == 4) 280 else if (k == 5) 400 else if (k == 6) 650 else if (k == 7) 60
|
|
161
|
+
else if (k == 8) 320 else if (k == 9) 500 else if (k == 10) 800 else 600
|
|
162
|
+
def posY(k: int): int =
|
|
163
|
+
if (k == 0) 130 else if (k == 1) 131 else if (k == 2) 130 else if (k == 3) 130
|
|
164
|
+
else if (k == 4) 170 else if (k == 5) 250 else if (k == 6) 450 else if (k == 7) 60
|
|
165
|
+
else if (k == 8) 220 else if (k == 9) 350 else if (k == 10) 600 else 60
|
|
166
|
+
def ptrOf(c: int): int = c / 12 + 1
|
|
167
|
+
def posOf(c: int): int = c % 12
|
|
168
|
+
|
|
169
|
+
// ---- hit testing, topmost first (half-open boxes, fp) --------------------
|
|
170
|
+
def inBox(wx: int, wy: int, bx: int, by: int, w: int, h: int): bool =
|
|
171
|
+
and(wx >= bx * TONE, wx < (bx + w) * TONE, wy >= by * TONE, wy < (by + h) * TONE)
|
|
172
|
+
def hitAt(wx: int, wy: int, x0: int, y0: int, x1: int, y1: int, x2: int, y2: int): int =
|
|
173
|
+
if (inBox(wx, wy, x2, y2, NW(2), NH(2))) 2
|
|
174
|
+
else if (inBox(wx, wy, x1, y1, NW(1), NH(1))) 1
|
|
175
|
+
else if (inBox(wx, wy, x0, y0, NW(0), NH(0))) 0
|
|
176
|
+
else -1
|
|
177
|
+
|
|
178
|
+
// ---- marquee: contain (rect encloses the box) vs overlap (positive-area intersection)
|
|
179
|
+
def contained(rx0: int, ry0: int, rx1: int, ry1: int, bx: int, by: int, w: int, h: int): bool =
|
|
180
|
+
and(rx0 <= bx * TONE, (bx + w) * TONE <= rx1, ry0 <= by * TONE, (by + h) * TONE <= ry1)
|
|
181
|
+
def overlapped(rx0: int, ry0: int, rx1: int, ry1: int, bx: int, by: int, w: int, h: int): bool =
|
|
182
|
+
and(rx0 < (bx + w) * TONE, bx * TONE < rx1, ry0 < (by + h) * TONE, by * TONE < ry1)
|
|
183
|
+
def hitBy(m: str, rx0: int, ry0: int, rx1: int, ry1: int, bx: int, by: int, w: int, h: int): bool =
|
|
184
|
+
if (m == "contain") contained(rx0, ry0, rx1, ry1, bx, by, w, h)
|
|
185
|
+
else overlapped(rx0, ry0, rx1, ry1, bx, by, w, h)
|
|
186
|
+
def rectMask(m: str, rx0: int, ry0: int, rx1: int, ry1: int, x0: int, y0: int, x1: int, y1: int, x2: int, y2: int): int =
|
|
187
|
+
one(hitBy(m, rx0, ry0, rx1, ry1, x0, y0, NW(0), NH(0)))
|
|
188
|
+
+ 2 * one(hitBy(m, rx0, ry0, rx1, ry1, x1, y1, NW(1), NH(1)))
|
|
189
|
+
+ 4 * one(hitBy(m, rx0, ry0, rx1, ry1, x2, y2, NW(2), NH(2)))
|
|
190
|
+
// marquee between the world grab point (gxM, gyM) and the world pointer (wx, wy)
|
|
191
|
+
def maskBy(m: str, gxM: int, gyM: int, wx: int, wy: int, x0: int, y0: int, x1: int, y1: int, x2: int, y2: int): int =
|
|
192
|
+
rectMask(m, minI(gxM, wx), minI(gyM, wy), maxI(gxM, wx), maxI(gyM, wy), x0, y0, x1, y1, x2, y2)
|
|
193
|
+
// live selection while marquee-ing: (shift ? snapshot : nothing) + the hits
|
|
194
|
+
def liveSel(m: str, shiftB: bool, snapM: int, gxM: int, gyM: int, wx: int, wy: int, x0: int, y0: int, x1: int, y1: int, x2: int, y2: int): int =
|
|
195
|
+
maskOr(if (shiftB) snapM else 0, maskBy(m, gxM, gyM, wx, wy, x0, y0, x1, y1, x2, y2))
|
|
196
|
+
|
|
197
|
+
// ---- selection at pointerdown / pointerup --------------------------------
|
|
198
|
+
def selOnDown(hit: int, spaceB: bool, shiftB: bool, selM: int): int =
|
|
199
|
+
if (spaceB) selM
|
|
200
|
+
else if (hit >= 0) (if (bit(selM, hit)) selM else if (shiftB) maskOr(selM, pow2(hit)) else pow2(hit))
|
|
201
|
+
else (if (shiftB) selM else 0)
|
|
202
|
+
def selAfterUp(m: str, tg: int, shiftB: bool, selM: int, snapM: int): int =
|
|
203
|
+
if (m == "pointing")
|
|
204
|
+
(if (tg >= 0) (if (shiftB) toggleBit(snapM, tg) else pow2(tg)) else (if (shiftB) snapM else 0))
|
|
205
|
+
else selM
|
|
206
|
+
|
|
207
|
+
// ---- drag, snapping (threshold in SCREEN px, candidates and results in WORLD units)
|
|
208
|
+
def moved(xM: int, yM: int, dxM: int, dyM: int): bool =
|
|
209
|
+
(xM - dxM) * (xM - dxM) + (yM - dyM) * (yM - dyM) > DRAG_PX * DRAG_PX
|
|
210
|
+
def modeAfter(m: str, tg: int, mv: bool): str =
|
|
211
|
+
if (and(m == "pointing", mv)) (if (tg >= 0) "dragging" else "marquee") else m
|
|
212
|
+
def rawD(w: int, g: int): int = rdiv(w - g, TONE)
|
|
213
|
+
def snapNearest(v: int): int = rdiv(v, GRID) * GRID
|
|
214
|
+
def snapHit(v: int, on: bool, zf: int): bool =
|
|
215
|
+
and(on, absI(v - snapNearest(v)) * zf <= SNAP_PX * ZONE)
|
|
216
|
+
def snapNudge(v: int, on: bool, zf: int): int = if (snapHit(v, on, zf)) snapNearest(v) - v else 0
|
|
217
|
+
// base = the dragged node's committed position on this axis
|
|
218
|
+
def liveDD(w: int, g: int, base: int, on: bool, zf: int): int =
|
|
219
|
+
rawD(w, g) + snapNudge(base + rawD(w, g), on, zf)
|
|
220
|
+
def liveGuide(wx: int, gxM: int, bx: int, wy: int, gyM: int, by: int, on: bool, zf: int): int =
|
|
221
|
+
one(snapHit(bx + rawD(wx, gxM), on, zf)) + 2 * one(snapHit(by + rawD(wy, gyM), on, zf))
|
|
222
|
+
def clampPos(v: int, extent: int): int = clampI(v, -WB, WB - extent)
|
|
223
|
+
def committed(i: int, v: int, d: int, selM: int, m: str, extent: int): int =
|
|
224
|
+
if (and(m == "dragging", bit(selM, i))) clampPos(v + d, extent) else v
|
|
225
|
+
|
|
226
|
+
// ---- the legal mode graph --------------------------------------------------
|
|
227
|
+
def legalEdge(a: str, b: str): bool =
|
|
228
|
+
or(
|
|
229
|
+
a == b,
|
|
230
|
+
and(a == "idle", or(b == "pointing", b == "panning")),
|
|
231
|
+
and(a == "pointing", or(b == "marquee", b == "dragging", b == "idle")),
|
|
232
|
+
and(or(a == "panning", a == "marquee", a == "dragging"), b == "idle")
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
// ---- derived views of the state (used by the invariants) -------------------
|
|
236
|
+
def curWx: int = screenToWorld(px, tx, z)
|
|
237
|
+
def curWy: int = screenToWorld(py, ty, z)
|
|
238
|
+
def primX: int = pick(tgt, n0x, n1x, n2x)
|
|
239
|
+
def primY: int = pick(tgt, n0y, n1y, n2y)
|
|
240
|
+
// live (on screen) node origin: a dragged node is its committed position + the live delta
|
|
241
|
+
def liveX(i: int): int = pick(i, n0x, n1x, n2x) + (if (and(mode == "dragging", bit(sel, i))) ddx else 0)
|
|
242
|
+
def liveY(i: int): int = pick(i, n0y, n1y, n2y) + (if (and(mode == "dragging", bit(sel, i))) ddy else 0)
|
|
243
|
+
def inView(i: int): bool =
|
|
244
|
+
and(
|
|
245
|
+
liveX(i) * TONE < screenToWorld(VW, tx, z), (liveX(i) + NW(i)) * TONE > screenToWorld(0, tx, z),
|
|
246
|
+
liveY(i) * TONE < screenToWorld(VH, ty, z), (liveY(i) + NH(i)) * TONE > screenToWorld(0, ty, z)
|
|
247
|
+
)
|
|
248
|
+
def exempt(i: int): bool = or(bit(sel, i), cursor == i, editing == i)
|
|
249
|
+
def culled(i: int): bool = and(not(inView(i)), not(exempt(i)))
|
|
250
|
+
|
|
251
|
+
// A press whose TOPMOST hit is the node being edited is NOT a gesture: it must leave the machine
|
|
252
|
+
// idle and keep `editing`, so a mouse press inside a text editor selects text instead of starting
|
|
253
|
+
// a drag. `pointerdown` in the primitive mirrors this by returning early. A press anywhere else
|
|
254
|
+
// still ends editing (the `editing' = -1` case below).
|
|
255
|
+
def inEditor(t: int): bool =
|
|
256
|
+
and(editing >= 0, hitAt(screenToWorld(posX(posOf(t)), tx, z), screenToWorld(posY(posOf(t)), ty, z), n0x, n0y, n1x, n1y, n2x, n2y) == editing)
|
|
257
|
+
def downGesture(t: int): bool = and(mode == "idle", not(inEditor(t)))
|
|
258
|
+
def nodeOk(x: int, y: int, i: int): bool =
|
|
259
|
+
and(x >= -WB, x + NW(i) <= WB, y >= -WB, y + NH(i) <= WB)
|
|
260
|
+
// deviation of the grabbed point from the pointer on one axis, in fp, against its allowance
|
|
261
|
+
def grabDevOk(w: int, g: int, d: int, snapped: bool, zf: int): bool =
|
|
262
|
+
if (snapped) 2 * absI(w - g - d * TONE) * zf <= TONE * zf + 2 * SNAP_PX * ZONE * TONE
|
|
263
|
+
else 2 * absI(w - g - d * TONE) <= TONE
|
|
264
|
+
def axisGrab(s: int, g: int, t: int, zf: int): bool =
|
|
265
|
+
or(absI(t) == WT, absI(screenToWorld(s, t, zf) - g) < TONE)
|
|
266
|
+
|
|
267
|
+
// host_move pool: place = 3 * k + node. Node `place % 3` takes position k = place / 3: the origin, a spot
|
|
268
|
+
// that covers palette 5, off screen (negative), and one far past the world bound (clamped).
|
|
269
|
+
def extX(k: int): int = if (k == 0) 0 else if (k == 1) 320 else if (k == 2) -300 else 5000000
|
|
270
|
+
def extY(k: int): int = if (k == 0) 0 else if (k == 1) 200 else if (k == 2) -300 else -5000000
|
|
271
|
+
def hostX(place: int, i: int, v: int): int = if (place % 3 == i) clampPos(extX(place / 3), NW(i)) else v
|
|
272
|
+
def hostY(place: int, i: int, v: int): int = if (place % 3 == i) clampPos(extY(place / 3), NH(i)) else v
|
|
273
|
+
|
|
274
|
+
action init = all {
|
|
275
|
+
mode' = "idle",
|
|
276
|
+
owner' = 0,
|
|
277
|
+
prev' = "idle",
|
|
278
|
+
tgt' = -1,
|
|
279
|
+
shift' = false,
|
|
280
|
+
space' = false,
|
|
281
|
+
mqmode' = "contain",
|
|
282
|
+
snapOn' = false,
|
|
283
|
+
tx' = 0,
|
|
284
|
+
ty' = 0,
|
|
285
|
+
z' = 4096,
|
|
286
|
+
px' = 0,
|
|
287
|
+
py' = 0,
|
|
288
|
+
downx' = 0,
|
|
289
|
+
downy' = 0,
|
|
290
|
+
gx' = 0,
|
|
291
|
+
gy' = 0,
|
|
292
|
+
sel' = 0,
|
|
293
|
+
selSnap' = 0,
|
|
294
|
+
cursor' = -1,
|
|
295
|
+
editing' = -1,
|
|
296
|
+
n0x' = 100,
|
|
297
|
+
n0y' = 100,
|
|
298
|
+
n1x' = 250,
|
|
299
|
+
n1y' = 150,
|
|
300
|
+
n2x' = 600,
|
|
301
|
+
n2y' = 400,
|
|
302
|
+
ddx' = 0,
|
|
303
|
+
ddy' = 0,
|
|
304
|
+
guide' = 0,
|
|
305
|
+
hostMoved' = false,
|
|
306
|
+
annCount' = 0,
|
|
307
|
+
annSel' = 0
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// pointerdown: touch = (pointer id - 1) * 12 + palette position. Ignored unless idle, so a
|
|
311
|
+
// second pointer can never start a gesture while one holds the capture. The hit is the TOPMOST
|
|
312
|
+
// node under the pointer in WORLD space; the grab point g is stored in world space.
|
|
313
|
+
action down = {
|
|
314
|
+
nondet touch = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23).oneOf()
|
|
315
|
+
all {
|
|
316
|
+
mode' = if (downGesture(touch)) (if (space) "panning" else "pointing") else mode,
|
|
317
|
+
owner' = if (downGesture(touch)) ptrOf(touch) else owner,
|
|
318
|
+
prev' = mode,
|
|
319
|
+
tgt' = if (and(downGesture(touch), not(space))) hitAt(screenToWorld(posX(posOf(touch)), tx, z), screenToWorld(posY(posOf(touch)), ty, z), n0x, n0y, n1x, n1y, n2x, n2y) else tgt,
|
|
320
|
+
shift' = shift,
|
|
321
|
+
space' = space,
|
|
322
|
+
mqmode' = mqmode,
|
|
323
|
+
snapOn' = snapOn,
|
|
324
|
+
tx' = tx,
|
|
325
|
+
ty' = ty,
|
|
326
|
+
z' = z,
|
|
327
|
+
px' = if (downGesture(touch)) posX(posOf(touch)) else px,
|
|
328
|
+
py' = if (downGesture(touch)) posY(posOf(touch)) else py,
|
|
329
|
+
downx' = if (downGesture(touch)) posX(posOf(touch)) else downx,
|
|
330
|
+
downy' = if (downGesture(touch)) posY(posOf(touch)) else downy,
|
|
331
|
+
gx' = if (downGesture(touch)) screenToWorld(posX(posOf(touch)), tx, z) else gx,
|
|
332
|
+
gy' = if (downGesture(touch)) screenToWorld(posY(posOf(touch)), ty, z) else gy,
|
|
333
|
+
sel' = if (downGesture(touch)) selOnDown(hitAt(screenToWorld(posX(posOf(touch)), tx, z), screenToWorld(posY(posOf(touch)), ty, z), n0x, n0y, n1x, n1y, n2x, n2y), space, shift, sel) else sel,
|
|
334
|
+
selSnap' = if (downGesture(touch)) sel else selSnap,
|
|
335
|
+
cursor' = if (and(downGesture(touch), not(space), hitAt(screenToWorld(posX(posOf(touch)), tx, z), screenToWorld(posY(posOf(touch)), ty, z), n0x, n0y, n1x, n1y, n2x, n2y) >= 0)) hitAt(screenToWorld(posX(posOf(touch)), tx, z), screenToWorld(posY(posOf(touch)), ty, z), n0x, n0y, n1x, n1y, n2x, n2y) else cursor,
|
|
336
|
+
editing' = if (downGesture(touch)) -1 else editing,
|
|
337
|
+
n0x' = n0x,
|
|
338
|
+
n0y' = n0y,
|
|
339
|
+
n1x' = n1x,
|
|
340
|
+
n1y' = n1y,
|
|
341
|
+
n2x' = n2x,
|
|
342
|
+
n2y' = n2y,
|
|
343
|
+
ddx' = ddx,
|
|
344
|
+
ddy' = ddy,
|
|
345
|
+
guide' = guide,
|
|
346
|
+
hostMoved' = hostMoved,
|
|
347
|
+
annCount' = annCount,
|
|
348
|
+
annSel' = annSel
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// pointermove (same touch encoding). Only the capture owner drives a gesture; when idle it just
|
|
353
|
+
// tracks the hover position. The drag threshold is in SCREEN px; the delta is recomputed from the
|
|
354
|
+
// pointerdown snapshot every time, in WORLD space (snapshot + delta, not an accumulation).
|
|
355
|
+
action move = {
|
|
356
|
+
nondet touch = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23).oneOf()
|
|
357
|
+
all {
|
|
358
|
+
mode' = if (and(mode != "idle", ptrOf(touch) == owner)) modeAfter(mode, tgt, moved(posX(posOf(touch)), posY(posOf(touch)), downx, downy)) else mode,
|
|
359
|
+
owner' = owner,
|
|
360
|
+
prev' = mode,
|
|
361
|
+
tgt' = tgt,
|
|
362
|
+
shift' = shift,
|
|
363
|
+
space' = space,
|
|
364
|
+
mqmode' = mqmode,
|
|
365
|
+
snapOn' = snapOn,
|
|
366
|
+
tx' = if (and(and(mode != "idle", ptrOf(touch) == owner), mode == "panning")) panGrab(posX(posOf(touch)), z, gx) else tx,
|
|
367
|
+
ty' = if (and(and(mode != "idle", ptrOf(touch) == owner), mode == "panning")) panGrab(posY(posOf(touch)), z, gy) else ty,
|
|
368
|
+
z' = z,
|
|
369
|
+
px' = if (or(mode == "idle", and(mode != "idle", ptrOf(touch) == owner))) posX(posOf(touch)) else px,
|
|
370
|
+
py' = if (or(mode == "idle", and(mode != "idle", ptrOf(touch) == owner))) posY(posOf(touch)) else py,
|
|
371
|
+
downx' = downx,
|
|
372
|
+
downy' = downy,
|
|
373
|
+
gx' = gx,
|
|
374
|
+
gy' = gy,
|
|
375
|
+
sel' = if (and(and(mode != "idle", ptrOf(touch) == owner), modeAfter(mode, tgt, moved(posX(posOf(touch)), posY(posOf(touch)), downx, downy)) == "marquee")) liveSel(mqmode, shift, selSnap, gx, gy, screenToWorld(posX(posOf(touch)), tx, z), screenToWorld(posY(posOf(touch)), ty, z), n0x, n0y, n1x, n1y, n2x, n2y) else sel,
|
|
376
|
+
selSnap' = selSnap,
|
|
377
|
+
cursor' = cursor,
|
|
378
|
+
editing' = editing,
|
|
379
|
+
n0x' = n0x,
|
|
380
|
+
n0y' = n0y,
|
|
381
|
+
n1x' = n1x,
|
|
382
|
+
n1y' = n1y,
|
|
383
|
+
n2x' = n2x,
|
|
384
|
+
n2y' = n2y,
|
|
385
|
+
ddx' = if (and(and(mode != "idle", ptrOf(touch) == owner), modeAfter(mode, tgt, moved(posX(posOf(touch)), posY(posOf(touch)), downx, downy)) == "dragging")) liveDD(screenToWorld(posX(posOf(touch)), tx, z), gx, pick(tgt, n0x, n1x, n2x), snapOn, z) else ddx,
|
|
386
|
+
ddy' = if (and(and(mode != "idle", ptrOf(touch) == owner), modeAfter(mode, tgt, moved(posX(posOf(touch)), posY(posOf(touch)), downx, downy)) == "dragging")) liveDD(screenToWorld(posY(posOf(touch)), ty, z), gy, pick(tgt, n0y, n1y, n2y), snapOn, z) else ddy,
|
|
387
|
+
guide' = if (and(and(mode != "idle", ptrOf(touch) == owner), modeAfter(mode, tgt, moved(posX(posOf(touch)), posY(posOf(touch)), downx, downy)) == "dragging")) liveGuide(screenToWorld(posX(posOf(touch)), tx, z), gx, pick(tgt, n0x, n1x, n2x), screenToWorld(posY(posOf(touch)), ty, z), gy, pick(tgt, n0y, n1y, n2y), snapOn, z) else guide,
|
|
388
|
+
hostMoved' = hostMoved,
|
|
389
|
+
annCount' = annCount,
|
|
390
|
+
annSel' = annSel
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// pointerup from pointer `lift`: only the owner ends the gesture. A click selects / toggles, a drag
|
|
395
|
+
// commits snapshot + delta (clamped into the world bound), a marquee keeps its live selection. The
|
|
396
|
+
// selection is ANNOUNCED here (never mid-gesture): annCount++ when it changed.
|
|
397
|
+
action up = {
|
|
398
|
+
nondet lift = Set(1, 2).oneOf()
|
|
399
|
+
all {
|
|
400
|
+
mode' = if (and(mode != "idle", lift == owner)) "idle" else mode,
|
|
401
|
+
owner' = if (and(mode != "idle", lift == owner)) 0 else owner,
|
|
402
|
+
prev' = mode,
|
|
403
|
+
tgt' = if (and(mode != "idle", lift == owner)) -1 else tgt,
|
|
404
|
+
shift' = shift,
|
|
405
|
+
space' = space,
|
|
406
|
+
mqmode' = mqmode,
|
|
407
|
+
snapOn' = snapOn,
|
|
408
|
+
tx' = tx,
|
|
409
|
+
ty' = ty,
|
|
410
|
+
z' = z,
|
|
411
|
+
px' = px,
|
|
412
|
+
py' = py,
|
|
413
|
+
downx' = downx,
|
|
414
|
+
downy' = downy,
|
|
415
|
+
gx' = gx,
|
|
416
|
+
gy' = gy,
|
|
417
|
+
sel' = if (and(mode != "idle", lift == owner)) selAfterUp(mode, tgt, shift, sel, selSnap) else sel,
|
|
418
|
+
selSnap' = if (and(mode != "idle", lift == owner)) selAfterUp(mode, tgt, shift, sel, selSnap) else selSnap,
|
|
419
|
+
cursor' = cursor,
|
|
420
|
+
editing' = editing,
|
|
421
|
+
n0x' = if (and(mode != "idle", lift == owner)) committed(0, n0x, ddx, sel, mode, NW(0)) else n0x,
|
|
422
|
+
n0y' = if (and(mode != "idle", lift == owner)) committed(0, n0y, ddy, sel, mode, NH(0)) else n0y,
|
|
423
|
+
n1x' = if (and(mode != "idle", lift == owner)) committed(1, n1x, ddx, sel, mode, NW(1)) else n1x,
|
|
424
|
+
n1y' = if (and(mode != "idle", lift == owner)) committed(1, n1y, ddy, sel, mode, NH(1)) else n1y,
|
|
425
|
+
n2x' = if (and(mode != "idle", lift == owner)) committed(2, n2x, ddx, sel, mode, NW(2)) else n2x,
|
|
426
|
+
n2y' = if (and(mode != "idle", lift == owner)) committed(2, n2y, ddy, sel, mode, NH(2)) else n2y,
|
|
427
|
+
ddx' = if (and(mode != "idle", lift == owner)) 0 else ddx,
|
|
428
|
+
ddy' = if (and(mode != "idle", lift == owner)) 0 else ddy,
|
|
429
|
+
guide' = if (and(mode != "idle", lift == owner)) 0 else guide,
|
|
430
|
+
hostMoved' = if (and(mode != "idle", lift == owner)) false else hostMoved,
|
|
431
|
+
annCount' = if (and(and(mode != "idle", lift == owner), selAfterUp(mode, tgt, shift, sel, selSnap) != annSel)) annCount + 1 else annCount,
|
|
432
|
+
annSel' = if (and(mode != "idle", lift == owner)) selAfterUp(mode, tgt, shift, sel, selSnap) else annSel
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// pointercancel / lost capture (palm rejection, the browser took the pointer): abort to the
|
|
437
|
+
// pointerdown snapshot. Nothing was committed, so the selection snapshot is all that is restored.
|
|
438
|
+
action cancel_pointer = {
|
|
439
|
+
nondet lost = Set(1, 2).oneOf()
|
|
440
|
+
all {
|
|
441
|
+
mode' = if (and(mode != "idle", lost == owner)) "idle" else mode,
|
|
442
|
+
owner' = if (and(mode != "idle", lost == owner)) 0 else owner,
|
|
443
|
+
prev' = mode,
|
|
444
|
+
tgt' = if (and(mode != "idle", lost == owner)) -1 else tgt,
|
|
445
|
+
shift' = shift,
|
|
446
|
+
space' = space,
|
|
447
|
+
mqmode' = mqmode,
|
|
448
|
+
snapOn' = snapOn,
|
|
449
|
+
tx' = tx,
|
|
450
|
+
ty' = ty,
|
|
451
|
+
z' = z,
|
|
452
|
+
px' = px,
|
|
453
|
+
py' = py,
|
|
454
|
+
downx' = downx,
|
|
455
|
+
downy' = downy,
|
|
456
|
+
gx' = gx,
|
|
457
|
+
gy' = gy,
|
|
458
|
+
sel' = if (and(mode != "idle", lost == owner)) selSnap else sel,
|
|
459
|
+
selSnap' = selSnap,
|
|
460
|
+
cursor' = cursor,
|
|
461
|
+
editing' = editing,
|
|
462
|
+
n0x' = n0x,
|
|
463
|
+
n0y' = n0y,
|
|
464
|
+
n1x' = n1x,
|
|
465
|
+
n1y' = n1y,
|
|
466
|
+
n2x' = n2x,
|
|
467
|
+
n2y' = n2y,
|
|
468
|
+
ddx' = if (and(mode != "idle", lost == owner)) 0 else ddx,
|
|
469
|
+
ddy' = if (and(mode != "idle", lost == owner)) 0 else ddy,
|
|
470
|
+
guide' = if (and(mode != "idle", lost == owner)) 0 else guide,
|
|
471
|
+
hostMoved' = if (and(mode != "idle", lost == owner)) false else hostMoved,
|
|
472
|
+
annCount' = if (and(and(mode != "idle", lost == owner), selSnap != annSel)) annCount + 1 else annCount,
|
|
473
|
+
annSel' = if (and(mode != "idle", lost == owner)) selSnap else annSel
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// modifiers and parameters are sampled at pointerdown: changes mid-gesture are ignored
|
|
478
|
+
action set_shift = {
|
|
479
|
+
nondet held = Set(true, false).oneOf()
|
|
480
|
+
all {
|
|
481
|
+
mode' = mode,
|
|
482
|
+
owner' = owner,
|
|
483
|
+
prev' = mode,
|
|
484
|
+
tgt' = tgt,
|
|
485
|
+
shift' = if (mode == "idle") held else shift,
|
|
486
|
+
space' = space,
|
|
487
|
+
mqmode' = mqmode,
|
|
488
|
+
snapOn' = snapOn,
|
|
489
|
+
tx' = tx,
|
|
490
|
+
ty' = ty,
|
|
491
|
+
z' = z,
|
|
492
|
+
px' = px,
|
|
493
|
+
py' = py,
|
|
494
|
+
downx' = downx,
|
|
495
|
+
downy' = downy,
|
|
496
|
+
gx' = gx,
|
|
497
|
+
gy' = gy,
|
|
498
|
+
sel' = sel,
|
|
499
|
+
selSnap' = selSnap,
|
|
500
|
+
cursor' = cursor,
|
|
501
|
+
editing' = editing,
|
|
502
|
+
n0x' = n0x,
|
|
503
|
+
n0y' = n0y,
|
|
504
|
+
n1x' = n1x,
|
|
505
|
+
n1y' = n1y,
|
|
506
|
+
n2x' = n2x,
|
|
507
|
+
n2y' = n2y,
|
|
508
|
+
ddx' = ddx,
|
|
509
|
+
ddy' = ddy,
|
|
510
|
+
guide' = guide,
|
|
511
|
+
hostMoved' = hostMoved,
|
|
512
|
+
annCount' = annCount,
|
|
513
|
+
annSel' = annSel
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
action set_space = {
|
|
518
|
+
nondet held = Set(true, false).oneOf()
|
|
519
|
+
all {
|
|
520
|
+
mode' = mode,
|
|
521
|
+
owner' = owner,
|
|
522
|
+
prev' = mode,
|
|
523
|
+
tgt' = tgt,
|
|
524
|
+
shift' = shift,
|
|
525
|
+
space' = if (mode == "idle") held else space,
|
|
526
|
+
mqmode' = mqmode,
|
|
527
|
+
snapOn' = snapOn,
|
|
528
|
+
tx' = tx,
|
|
529
|
+
ty' = ty,
|
|
530
|
+
z' = z,
|
|
531
|
+
px' = px,
|
|
532
|
+
py' = py,
|
|
533
|
+
downx' = downx,
|
|
534
|
+
downy' = downy,
|
|
535
|
+
gx' = gx,
|
|
536
|
+
gy' = gy,
|
|
537
|
+
sel' = sel,
|
|
538
|
+
selSnap' = selSnap,
|
|
539
|
+
cursor' = cursor,
|
|
540
|
+
editing' = editing,
|
|
541
|
+
n0x' = n0x,
|
|
542
|
+
n0y' = n0y,
|
|
543
|
+
n1x' = n1x,
|
|
544
|
+
n1y' = n1y,
|
|
545
|
+
n2x' = n2x,
|
|
546
|
+
n2y' = n2y,
|
|
547
|
+
ddx' = ddx,
|
|
548
|
+
ddy' = ddy,
|
|
549
|
+
guide' = guide,
|
|
550
|
+
hostMoved' = hostMoved,
|
|
551
|
+
annCount' = annCount,
|
|
552
|
+
annSel' = annSel
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
action set_marquee_mode = {
|
|
557
|
+
nondet choice = Set("contain", "overlap").oneOf()
|
|
558
|
+
all {
|
|
559
|
+
mode' = mode,
|
|
560
|
+
owner' = owner,
|
|
561
|
+
prev' = mode,
|
|
562
|
+
tgt' = tgt,
|
|
563
|
+
shift' = shift,
|
|
564
|
+
space' = space,
|
|
565
|
+
mqmode' = if (mode == "idle") choice else mqmode,
|
|
566
|
+
snapOn' = snapOn,
|
|
567
|
+
tx' = tx,
|
|
568
|
+
ty' = ty,
|
|
569
|
+
z' = z,
|
|
570
|
+
px' = px,
|
|
571
|
+
py' = py,
|
|
572
|
+
downx' = downx,
|
|
573
|
+
downy' = downy,
|
|
574
|
+
gx' = gx,
|
|
575
|
+
gy' = gy,
|
|
576
|
+
sel' = sel,
|
|
577
|
+
selSnap' = selSnap,
|
|
578
|
+
cursor' = cursor,
|
|
579
|
+
editing' = editing,
|
|
580
|
+
n0x' = n0x,
|
|
581
|
+
n0y' = n0y,
|
|
582
|
+
n1x' = n1x,
|
|
583
|
+
n1y' = n1y,
|
|
584
|
+
n2x' = n2x,
|
|
585
|
+
n2y' = n2y,
|
|
586
|
+
ddx' = ddx,
|
|
587
|
+
ddy' = ddy,
|
|
588
|
+
guide' = guide,
|
|
589
|
+
hostMoved' = hostMoved,
|
|
590
|
+
annCount' = annCount,
|
|
591
|
+
annSel' = annSel
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
action set_snap = {
|
|
596
|
+
nondet snapping = Set(true, false).oneOf()
|
|
597
|
+
all {
|
|
598
|
+
mode' = mode,
|
|
599
|
+
owner' = owner,
|
|
600
|
+
prev' = mode,
|
|
601
|
+
tgt' = tgt,
|
|
602
|
+
shift' = shift,
|
|
603
|
+
space' = space,
|
|
604
|
+
mqmode' = mqmode,
|
|
605
|
+
snapOn' = if (mode == "idle") snapping else snapOn,
|
|
606
|
+
tx' = tx,
|
|
607
|
+
ty' = ty,
|
|
608
|
+
z' = z,
|
|
609
|
+
px' = px,
|
|
610
|
+
py' = py,
|
|
611
|
+
downx' = downx,
|
|
612
|
+
downy' = downy,
|
|
613
|
+
gx' = gx,
|
|
614
|
+
gy' = gy,
|
|
615
|
+
sel' = sel,
|
|
616
|
+
selSnap' = selSnap,
|
|
617
|
+
cursor' = cursor,
|
|
618
|
+
editing' = editing,
|
|
619
|
+
n0x' = n0x,
|
|
620
|
+
n0y' = n0y,
|
|
621
|
+
n1x' = n1x,
|
|
622
|
+
n1y' = n1y,
|
|
623
|
+
n2x' = n2x,
|
|
624
|
+
n2y' = n2y,
|
|
625
|
+
ddx' = ddx,
|
|
626
|
+
ddy' = ddy,
|
|
627
|
+
guide' = guide,
|
|
628
|
+
hostMoved' = hostMoved,
|
|
629
|
+
annCount' = annCount,
|
|
630
|
+
annSel' = annSel
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// wheel / pinch under a running gesture: zoom to a preset level about the POINTER. The pointer's
|
|
635
|
+
// world point is fixed, so a drag / marquee / pan composes with it; every derived gesture value
|
|
636
|
+
// is re-solved against the new camera (a panning camera re-grabs so it never keeps a stale offset).
|
|
637
|
+
action zoom_to = {
|
|
638
|
+
nondet level = Set(0, 1, 2, 3, 4).oneOf()
|
|
639
|
+
all {
|
|
640
|
+
mode' = mode,
|
|
641
|
+
owner' = owner,
|
|
642
|
+
prev' = mode,
|
|
643
|
+
tgt' = tgt,
|
|
644
|
+
shift' = shift,
|
|
645
|
+
space' = space,
|
|
646
|
+
mqmode' = mqmode,
|
|
647
|
+
snapOn' = snapOn,
|
|
648
|
+
tx' = (if (mode == "panning") panGrab(px, clampZ(zoomPreset(level)), gx) else anchoredT(tx, z, clampZ(zoomPreset(level)), px)),
|
|
649
|
+
ty' = (if (mode == "panning") panGrab(py, clampZ(zoomPreset(level)), gy) else anchoredT(ty, z, clampZ(zoomPreset(level)), py)),
|
|
650
|
+
z' = clampZ(zoomPreset(level)),
|
|
651
|
+
px' = px,
|
|
652
|
+
py' = py,
|
|
653
|
+
downx' = downx,
|
|
654
|
+
downy' = downy,
|
|
655
|
+
gx' = gx,
|
|
656
|
+
gy' = gy,
|
|
657
|
+
sel' = if (mode == "marquee") liveSel(mqmode, shift, selSnap, gx, gy, screenToWorld(px, if (mode == "panning") panGrab(px, clampZ(zoomPreset(level)), gx) else anchoredT(tx, z, clampZ(zoomPreset(level)), px), clampZ(zoomPreset(level))), screenToWorld(py, if (mode == "panning") panGrab(py, clampZ(zoomPreset(level)), gy) else anchoredT(ty, z, clampZ(zoomPreset(level)), py), clampZ(zoomPreset(level))), n0x, n0y, n1x, n1y, n2x, n2y) else sel,
|
|
658
|
+
selSnap' = selSnap,
|
|
659
|
+
cursor' = cursor,
|
|
660
|
+
editing' = editing,
|
|
661
|
+
n0x' = n0x,
|
|
662
|
+
n0y' = n0y,
|
|
663
|
+
n1x' = n1x,
|
|
664
|
+
n1y' = n1y,
|
|
665
|
+
n2x' = n2x,
|
|
666
|
+
n2y' = n2y,
|
|
667
|
+
ddx' = if (mode == "dragging") liveDD(screenToWorld(px, if (mode == "panning") panGrab(px, clampZ(zoomPreset(level)), gx) else anchoredT(tx, z, clampZ(zoomPreset(level)), px), clampZ(zoomPreset(level))), gx, pick(tgt, n0x, n1x, n2x), snapOn, clampZ(zoomPreset(level))) else ddx,
|
|
668
|
+
ddy' = if (mode == "dragging") liveDD(screenToWorld(py, if (mode == "panning") panGrab(py, clampZ(zoomPreset(level)), gy) else anchoredT(ty, z, clampZ(zoomPreset(level)), py), clampZ(zoomPreset(level))), gy, pick(tgt, n0y, n1y, n2y), snapOn, clampZ(zoomPreset(level))) else ddy,
|
|
669
|
+
guide' = if (mode == "dragging") liveGuide(screenToWorld(px, if (mode == "panning") panGrab(px, clampZ(zoomPreset(level)), gx) else anchoredT(tx, z, clampZ(zoomPreset(level)), px), clampZ(zoomPreset(level))), gx, pick(tgt, n0x, n1x, n2x), screenToWorld(py, if (mode == "panning") panGrab(py, clampZ(zoomPreset(level)), gy) else anchoredT(ty, z, clampZ(zoomPreset(level)), py), clampZ(zoomPreset(level))), gy, pick(tgt, n0y, n1y, n2y), snapOn, clampZ(zoomPreset(level))) else guide,
|
|
670
|
+
hostMoved' = hostMoved,
|
|
671
|
+
annCount' = annCount,
|
|
672
|
+
annSel' = annSel
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// enter / leave editing of the single selected node (only while idle)
|
|
677
|
+
action toggle_edit = all {
|
|
678
|
+
mode' = mode,
|
|
679
|
+
owner' = owner,
|
|
680
|
+
prev' = mode,
|
|
681
|
+
tgt' = tgt,
|
|
682
|
+
shift' = shift,
|
|
683
|
+
space' = space,
|
|
684
|
+
mqmode' = mqmode,
|
|
685
|
+
snapOn' = snapOn,
|
|
686
|
+
tx' = tx,
|
|
687
|
+
ty' = ty,
|
|
688
|
+
z' = z,
|
|
689
|
+
px' = px,
|
|
690
|
+
py' = py,
|
|
691
|
+
downx' = downx,
|
|
692
|
+
downy' = downy,
|
|
693
|
+
gx' = gx,
|
|
694
|
+
gy' = gy,
|
|
695
|
+
sel' = sel,
|
|
696
|
+
selSnap' = selSnap,
|
|
697
|
+
cursor' = if (and(mode == "idle", editing < 0, single(sel) >= 0)) single(sel) else cursor,
|
|
698
|
+
editing' = if (mode != "idle") editing else if (editing >= 0) -1 else single(sel),
|
|
699
|
+
n0x' = n0x,
|
|
700
|
+
n0y' = n0y,
|
|
701
|
+
n1x' = n1x,
|
|
702
|
+
n1y' = n1y,
|
|
703
|
+
n2x' = n2x,
|
|
704
|
+
n2y' = n2y,
|
|
705
|
+
ddx' = ddx,
|
|
706
|
+
ddy' = ddy,
|
|
707
|
+
guide' = guide,
|
|
708
|
+
hostMoved' = hostMoved,
|
|
709
|
+
annCount' = annCount,
|
|
710
|
+
annSel' = annSel
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// host_move: the HOST sets one node's COMMITTED position (an auto-layout, a controlled scene), in
|
|
714
|
+
// ANY mode, clamped into the world bound. A running drag or marquee is re-solved against the new
|
|
715
|
+
// geometry, like zoom_to does for the camera, so the derived values stay f(pointer, geometry).
|
|
716
|
+
// `hostMoved` records that it happened in this gesture: the target was the topmost hit at
|
|
717
|
+
// pointerdown, which a host move may since have changed. Sizes and the z-order are not modelled.
|
|
718
|
+
action host_move = {
|
|
719
|
+
nondet place = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11).oneOf()
|
|
720
|
+
all {
|
|
721
|
+
mode' = mode,
|
|
722
|
+
owner' = owner,
|
|
723
|
+
prev' = mode,
|
|
724
|
+
tgt' = tgt,
|
|
725
|
+
shift' = shift,
|
|
726
|
+
space' = space,
|
|
727
|
+
mqmode' = mqmode,
|
|
728
|
+
snapOn' = snapOn,
|
|
729
|
+
tx' = tx,
|
|
730
|
+
ty' = ty,
|
|
731
|
+
z' = z,
|
|
732
|
+
px' = px,
|
|
733
|
+
py' = py,
|
|
734
|
+
downx' = downx,
|
|
735
|
+
downy' = downy,
|
|
736
|
+
gx' = gx,
|
|
737
|
+
gy' = gy,
|
|
738
|
+
sel' = if (mode == "marquee") liveSel(mqmode, shift, selSnap, gx, gy, screenToWorld(px, tx, z), screenToWorld(py, ty, z), hostX(place, 0, n0x), hostY(place, 0, n0y), hostX(place, 1, n1x), hostY(place, 1, n1y), hostX(place, 2, n2x), hostY(place, 2, n2y)) else sel,
|
|
739
|
+
selSnap' = selSnap,
|
|
740
|
+
cursor' = cursor,
|
|
741
|
+
editing' = editing,
|
|
742
|
+
n0x' = hostX(place, 0, n0x),
|
|
743
|
+
n0y' = hostY(place, 0, n0y),
|
|
744
|
+
n1x' = hostX(place, 1, n1x),
|
|
745
|
+
n1y' = hostY(place, 1, n1y),
|
|
746
|
+
n2x' = hostX(place, 2, n2x),
|
|
747
|
+
n2y' = hostY(place, 2, n2y),
|
|
748
|
+
ddx' = if (mode == "dragging") liveDD(screenToWorld(px, tx, z), gx, pick(tgt, hostX(place, 0, n0x), hostX(place, 1, n1x), hostX(place, 2, n2x)), snapOn, z) else ddx,
|
|
749
|
+
ddy' = if (mode == "dragging") liveDD(screenToWorld(py, ty, z), gy, pick(tgt, hostY(place, 0, n0y), hostY(place, 1, n1y), hostY(place, 2, n2y)), snapOn, z) else ddy,
|
|
750
|
+
guide' = if (mode == "dragging") liveGuide(screenToWorld(px, tx, z), gx, pick(tgt, hostX(place, 0, n0x), hostX(place, 1, n1x), hostX(place, 2, n2x)), screenToWorld(py, ty, z), gy, pick(tgt, hostY(place, 0, n0y), hostY(place, 1, n1y), hostY(place, 2, n2y)), snapOn, z) else guide,
|
|
751
|
+
hostMoved' = or(hostMoved, mode != "idle"),
|
|
752
|
+
annCount' = annCount,
|
|
753
|
+
annSel' = annSel
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
action step = any { init, down, move, up, cancel_pointer, set_shift, set_space, set_marquee_mode, set_snap, zoom_to, toggle_edit, host_move }
|
|
758
|
+
|
|
759
|
+
val ModeValid: bool =
|
|
760
|
+
or(mode == "idle", mode == "pointing", mode == "panning", mode == "marquee", mode == "dragging")
|
|
761
|
+
// exactly one capture owner while a gesture runs, nobody otherwise
|
|
762
|
+
val OneCaptureOwner: bool = and((mode == "idle") == (owner == 0), owner >= 0, owner <= 2)
|
|
763
|
+
// a gesture starts only from idle; the others only follow the legal edges
|
|
764
|
+
val OnlyLegalTransitions: bool = legalEdge(prev, mode)
|
|
765
|
+
// selection (and its snapshot / announcement) is a subset of the 3 nodes
|
|
766
|
+
val SelectionSubsetOfNodes: bool =
|
|
767
|
+
and(sel >= 0, sel <= 7, selSnap >= 0, selSnap <= 7, annSel >= 0, annSel <= 7)
|
|
768
|
+
val IdsInRange: bool =
|
|
769
|
+
and(tgt >= -1, tgt <= 2, cursor >= -1, cursor <= 2, editing >= -1, editing <= 2)
|
|
770
|
+
// idle carries no gesture residue and the selection snapshot is the selection
|
|
771
|
+
val IdleIsClean: bool =
|
|
772
|
+
or(mode != "idle", and(tgt == -1, ddx == 0, ddy == 0, guide == 0, selSnap == sel, not(hostMoved)))
|
|
773
|
+
val GestureTargetConsistent: bool = and(
|
|
774
|
+
or(mode != "pointing", and(ddx == 0, ddy == 0, guide == 0)),
|
|
775
|
+
or(mode != "marquee", and(tgt == -1, ddx == 0, ddy == 0, guide == 0)),
|
|
776
|
+
or(mode != "panning", and(tgt == -1, ddx == 0, ddy == 0, guide == 0)),
|
|
777
|
+
or(mode != "dragging", and(tgt >= 0, tgt <= 2, bit(sel, tgt)))
|
|
778
|
+
)
|
|
779
|
+
// below the threshold (screen px, squared) a press has not become a gesture yet
|
|
780
|
+
val PointingWithinThreshold: bool = or(mode != "pointing", not(moved(px, py, downx, downy)))
|
|
781
|
+
// the gesture target is the topmost node under the pointer at pointerdown (world hit test); a
|
|
782
|
+
// host_move during the gesture (hostMoved) may have moved it since, so the check stands down
|
|
783
|
+
val TargetIsTopmostHit: bool =
|
|
784
|
+
or(mode == "idle", mode == "panning", hostMoved, tgt == hitAt(gx, gy, n0x, n0y, n1x, n1y, n2x, n2y))
|
|
785
|
+
// drag = snapshot + world delta: the stored delta equals f(world pointer, world grab point)
|
|
786
|
+
val DragIsSnapshotPlusDelta: bool = or(mode != "dragging", and(
|
|
787
|
+
ddx == liveDD(curWx, gx, primX, snapOn, z),
|
|
788
|
+
ddy == liveDD(curWy, gy, primY, snapOn, z),
|
|
789
|
+
guide == liveGuide(curWx, gx, primX, curWy, gy, primY, snapOn, z)
|
|
790
|
+
))
|
|
791
|
+
// the grabbed point stays under the pointer (half a unit; plus the snap distance when snapped)
|
|
792
|
+
val DragPreservesGrabOffset: bool = or(mode != "dragging", and(
|
|
793
|
+
grabDevOk(curWx, gx, ddx, bit(guide, 0), z),
|
|
794
|
+
grabDevOk(curWy, gy, ddy, bit(guide, 1), z)
|
|
795
|
+
))
|
|
796
|
+
// a snapped axis sits on the grid; an unsnapped axis was not within the screen threshold
|
|
797
|
+
val SnappedPositionOnGrid: bool = or(mode != "dragging", and(
|
|
798
|
+
or(not(bit(guide, 0)), (primX + ddx) % GRID == 0),
|
|
799
|
+
or(not(bit(guide, 1)), (primY + ddy) % GRID == 0)
|
|
800
|
+
))
|
|
801
|
+
val NoMissedSnap: bool = or(mode != "dragging", not(snapOn), and(
|
|
802
|
+
bit(guide, 0) == snapHit(primX + ddx, true, z),
|
|
803
|
+
bit(guide, 1) == snapHit(primY + ddy, true, z)
|
|
804
|
+
))
|
|
805
|
+
// guides are output state: only while dragging with snapping on
|
|
806
|
+
val GuidesOnlyWhileSnapping: bool = or(and(mode == "dragging", snapOn), guide == 0)
|
|
807
|
+
// panning keeps the grab world point under the pointer (unless the world bound pins the camera)
|
|
808
|
+
val PanKeepsGrabUnderPointer: bool =
|
|
809
|
+
or(mode != "panning", and(axisGrab(px, gx, tx, z), axisGrab(py, gy, ty, z)))
|
|
810
|
+
val CameraInBound: bool =
|
|
811
|
+
and(ZMIN > 0, z >= ZMIN, z <= ZMAX, absI(tx) <= WT, absI(ty) <= WT)
|
|
812
|
+
val NodesInWorldBound: bool =
|
|
813
|
+
and(nodeOk(n0x, n0y, 0), nodeOk(n1x, n1y, 1), nodeOk(n2x, n2y, 2))
|
|
814
|
+
// marquee monotonicity: whatever the rectangle, contain selects a subset of overlap
|
|
815
|
+
val ContainSubsetOfOverlap: bool =
|
|
816
|
+
maskOr(
|
|
817
|
+
maskBy("contain", gx, gy, curWx, curWy, n0x, n0y, n1x, n1y, n2x, n2y),
|
|
818
|
+
maskBy("overlap", gx, gy, curWx, curWy, n0x, n0y, n1x, n1y, n2x, n2y)
|
|
819
|
+
) == maskBy("overlap", gx, gy, curWx, curWy, n0x, n0y, n1x, n1y, n2x, n2y)
|
|
820
|
+
// while marquee-ing the selection is exactly the mode's hits, on top of the base when additive
|
|
821
|
+
val MarqueeSelectionExact: bool = or(mode != "marquee",
|
|
822
|
+
sel == liveSel(mqmode, shift, selSnap, gx, gy, curWx, curWy, n0x, n0y, n1x, n1y, n2x, n2y))
|
|
823
|
+
val MarqueeAdditiveKeepsBase: bool =
|
|
824
|
+
or(mode != "marquee", not(shift), maskOr(sel, selSnap) == sel)
|
|
825
|
+
// selected / cursor / editing nodes are never culled, and a culled node is off screen
|
|
826
|
+
val ExemptNodesNeverCulled: bool = and(
|
|
827
|
+
or(not(exempt(0)), not(culled(0))),
|
|
828
|
+
or(not(exempt(1)), not(culled(1))),
|
|
829
|
+
or(not(exempt(2)), not(culled(2)))
|
|
830
|
+
)
|
|
831
|
+
val CulledOnlyWhenOffscreen: bool = and(
|
|
832
|
+
or(not(culled(0)), not(inView(0))),
|
|
833
|
+
or(not(culled(1)), not(inView(1))),
|
|
834
|
+
or(not(culled(2)), not(inView(2)))
|
|
835
|
+
)
|
|
836
|
+
val EditingIsTheSingleSelection: bool =
|
|
837
|
+
or(editing == -1, and(mode == "idle", sel == pow2(editing), cursor == editing))
|
|
838
|
+
// the live region only ever carries the settled selection
|
|
839
|
+
val AnnouncementsTrackSelection: bool =
|
|
840
|
+
or(mode != "idle", and(annSel == sel, or(sel == 0, annCount >= 1)))
|
|
841
|
+
}
|