@yuneta/gobj-ui 7.17.0 → 7.18.0

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/README.md CHANGED
@@ -680,6 +680,8 @@ Two implementation notes worth keeping:
680
680
  | fit | `fitView()`: the whole graph in the viewport |
681
681
  | **`1:1`** | `zoomTo(1)`: actual size |
682
682
  | ── | |
683
+ | **fit to selection** | `fit`, for the part that is selected (edition only, disabled while nothing is) |
684
+ | ── | |
683
685
  | full screen | the container, not the camera |
684
686
 
685
687
  Two of those rows are the answer to a real complaint, and the reasoning
@@ -695,6 +697,14 @@ generalises:
695
697
  that offers it, because there is no glyph anybody recognises for it.
696
698
  - **the zoom level is shown** because `1:1` is a jump to a number, and a jump
697
699
  to a number is only meaningful next to the number you are on.
700
+ - **fit to selection** wears the `fit` icon with a marked object inside it,
701
+ drawn rather than borrowed: the two sit next to each other and are the same
702
+ action at two scopes, so they have to read as a family. It appears only in
703
+ edition — a button that can never be enabled is furniture, not a control —
704
+ and is disabled while nothing is selected. `fitView()` has no subset form,
705
+ so the bounds are measured off the elements and the zoom is clamped to the
706
+ graph's own `zoomRange`, the only limit that is not invented here. One card
707
+ filling the view is not a bug: that is what zooming to it means.
698
708
 
699
709
  The **separators are gaps, not lines**: every item already carries a hairline
700
710
  against its neighbour, so one more line would not group anything. Full screen
@@ -705,8 +715,9 @@ Both toolbars (this one and the edit one) **follow the theme**. They used to be
705
715
  pinned to a light background in both themes, with the icon colour pinned dark
706
716
  so it survived that — two light islands over a dark canvas.
707
717
 
708
- **New keys for consumers: `actual size`, `zoom level`** (both tooltips, so a
709
- host that has not defined them shows the key on hover and nothing else breaks).
718
+ **New keys for consumers: `actual size`, `zoom level`, `zoom to selection`**
719
+ (all tooltips, so a host that has not defined them shows the key on hover and
720
+ nothing else breaks).
710
721
 
711
722
  ### Selecting several nodes, and moving them together
712
723
 
@@ -719,6 +730,7 @@ clicked":
719
730
  | **shift + click** | adds that node to the selection, or takes it out |
720
731
  | **shift + drag on the canvas** | rubber band: the selection becomes what it enclosed |
721
732
  | **ctrl/cmd + A** | every node |
733
+ | the **fit-to-selection** button | puts the viewport on what is selected |
722
734
  | drag any selected node | **moves the whole selection**, as one undo |
723
735
  | **Delete** / **Backspace** | deletes the selection, after a confirmation that counts what it takes |
724
736
  | **Esc**, or a click on the canvas | clears it |
@@ -53617,6 +53617,15 @@ var ICONS = `<svg xmlns="http://www.w3.org/2000/svg">
53617
53617
  <symbol id="g6-icon-fit" viewBox="0 0 448 512">
53618
53618
  <path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"/>
53619
53619
  </symbol>
53620
+ <!-- fit to selection: the same corner brackets as the fit icon,
53621
+ around a marked object. The two sit side by side in the
53622
+ toolbar and have to read as a family, which a borrowed glyph
53623
+ would not. (No backticks in here: the sprite lives inside a
53624
+ template literal.) -->
53625
+ <symbol id="g6-icon-fit-selection" viewBox="0 0 448 512">
53626
+ <path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"/>
53627
+ <rect fill="currentColor" x="144" y="176" width="160" height="160" rx="24"/>
53628
+ </symbol>
53620
53629
  <!-- crosshairs (center) -->
53621
53630
  <symbol id="g6-icon-center" viewBox="0 0 576 512">
53622
53631
  <path fill="currentColor" d="M288-16c17.7 0 32 14.3 32 32l0 18.3c98.1 14 175.7 91.6 189.7 189.7l18.3 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-18.3 0c-14 98.1-91.6 175.7-189.7 189.7l0 18.3c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-18.3C157.9 463.7 80.3 386.1 66.3 288L48 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l18.3 0C80.3 125.9 157.9 48.3 256 34.3L256 16c0-17.7 14.3-32 32-32zM131.2 288c12.7 62.7 62.1 112.1 124.8 124.8l0-12.8c0-17.7 14.3-32 32-32s32 14.3 32 32l0 12.8c62.7-12.7 112.1-62.1 124.8-124.8L432 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l12.8 0C432.1 161.3 382.7 111.9 320 99.2l0 12.8c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-12.8C193.3 111.9 143.9 161.3 131.2 224l12.8 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-12.8 0zM288 208a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"/>
@@ -54150,6 +54159,7 @@ function update_toolbar(gobj) {
54150
54159
  if (graph_get_plugin(gobj, "toolbar")) graph.updatePlugin({ key: "toolbar" });
54151
54160
  configure_toolbar_edit(gobj);
54152
54161
  if (graph_get_plugin(gobj, "toolbar-edit")) graph.updatePlugin({ key: "toolbar-edit" });
54162
+ update_zoom_selection_button(gobj);
54153
54163
  }
54154
54164
  /************************************************************
54155
54165
  * The floating toolbars follow the theme.
@@ -54250,6 +54260,13 @@ function configure_toolbar(gobj) {
54250
54260
  title: (0, i18next.t)("actual size")
54251
54261
  }
54252
54262
  ];
54263
+ if (priv.edit_mode) items.push({
54264
+ id: "g6-icon-fit-selection",
54265
+ value: "fit-selection",
54266
+ className: "EV_ZOOM_SELECTION",
54267
+ title: (0, i18next.t)("zoom to selection"),
54268
+ disabled: true
54269
+ });
54253
54270
  if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_fullscreen")) {
54254
54271
  items.push({ separator: true });
54255
54272
  if (priv.is_fullscreen) items.push({
@@ -54281,6 +54298,9 @@ function configure_toolbar(gobj) {
54281
54298
  case "auto-fit":
54282
54299
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_AUTO_FIT", {}, gobj);
54283
54300
  break;
54301
+ case "fit-selection":
54302
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_ZOOM_SELECTION", {}, gobj);
54303
+ break;
54284
54304
  case "center":
54285
54305
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CENTER", {}, gobj);
54286
54306
  break;
@@ -55086,6 +55106,52 @@ async function graph_fit_readable(gobj) {
55086
55106
  }
55087
55107
  }
55088
55108
  /************************************************************
55109
+ * Put the viewport on the selection: what `fit` does, for a part.
55110
+ *
55111
+ * Computed rather than delegated because `fitView()` fits the whole
55112
+ * graph and there is no subset form of it. The bounds come from the
55113
+ * elements themselves, so a node's real card size is what is
55114
+ * measured, not an assumed one; the zoom is clamped to the graph's
55115
+ * own `zoomRange`, which is the only limit that is not invented
55116
+ * here. A selection of one card filling the view is not a bug --
55117
+ * that is what zooming to it means, and it is what every editor
55118
+ * that offers the action does.
55119
+ ************************************************************/
55120
+ async function graph_fit_selection(gobj, ids) {
55121
+ let graph = gobj.priv.graph;
55122
+ if (!graph || !ids || !ids.length) return;
55123
+ let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity;
55124
+ let found = 0;
55125
+ for (let id of ids) {
55126
+ let bounds;
55127
+ try {
55128
+ bounds = graph.getElementRenderBounds(id);
55129
+ } catch (e) {
55130
+ continue;
55131
+ }
55132
+ if (!bounds || !bounds.min || !bounds.max) continue;
55133
+ minx = Math.min(minx, bounds.min[0]);
55134
+ miny = Math.min(miny, bounds.min[1]);
55135
+ maxx = Math.max(maxx, bounds.max[0]);
55136
+ maxy = Math.max(maxy, bounds.max[1]);
55137
+ found++;
55138
+ }
55139
+ if (!found) {
55140
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: the selection has no bounds to fit`);
55141
+ return;
55142
+ }
55143
+ let size = graph.getSize();
55144
+ let vw = Math.max(1, size[0] - 96);
55145
+ let vh = Math.max(1, size[1] - 96);
55146
+ let bw = Math.max(1, maxx - minx);
55147
+ let bh = Math.max(1, maxy - miny);
55148
+ let zoom = Math.min(vw / bw, vh / bh);
55149
+ let range = graph.getZoomRange() || [.2, 4];
55150
+ zoom = Math.max(range[0], Math.min(range[1], zoom));
55151
+ await graph.zoomTo(zoom);
55152
+ await graph.focusElement(ids);
55153
+ }
55154
+ /************************************************************
55089
55155
  * Focus the graph on one topic: highlight (amber 'active' state)
55090
55156
  * every node of that topic and centre the viewport on them. An
55091
55157
  * empty topic clears the highlight. If the graph is not rendered
@@ -55554,6 +55620,23 @@ function paint_selection(gobj, ids) {
55554
55620
  let next = ids || [];
55555
55621
  priv._selected_paint_ids = next;
55556
55622
  repaint_cards(gobj, /* @__PURE__ */ new Set([...prev, ...next]));
55623
+ update_zoom_selection_button(gobj);
55624
+ }
55625
+ /************************************************************
55626
+ * Zoom-to-selection can only act on a selection.
55627
+ *
55628
+ * The attribute is toggled on the element instead of re-rendering
55629
+ * the toolbar, which would rebuild its innerHTML -- and with it
55630
+ * drop the disabled state of every OTHER button -- each time a
55631
+ * card is ticked.
55632
+ ************************************************************/
55633
+ function update_zoom_selection_button(gobj) {
55634
+ let priv = gobj.priv;
55635
+ let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
55636
+ if (!$container) return;
55637
+ if (!$container.querySelector(".EV_ZOOM_SELECTION")) return;
55638
+ if ((priv._selected_paint_ids || []).length > 0) enableElements($container, ".EV_ZOOM_SELECTION");
55639
+ else disableElements($container, ".EV_ZOOM_SELECTION");
55557
55640
  }
55558
55641
  /************************************************************
55559
55642
  * Select a SET of nodes: what a marquee and a shift-click do.
@@ -58058,6 +58141,18 @@ function ac_auto_fit(gobj, event, kw, src) {
58058
58141
  graph_fitview(gobj);
58059
58142
  return 0;
58060
58143
  }
58144
+ /************************************************************
58145
+ * Zoom to what is selected.
58146
+ ************************************************************/
58147
+ function ac_zoom_selection(gobj, event, kw, src) {
58148
+ let ids = gobj.priv._selected_paint_ids || [];
58149
+ if (!ids.length) {
58150
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: zoom to selection with nothing selected`);
58151
+ return -1;
58152
+ }
58153
+ graph_fit_selection(gobj, ids);
58154
+ return 0;
58155
+ }
58061
58156
  function ac_focus_topic(gobj, event, kw, src) {
58062
58157
  graph_focus_topic(gobj, kw && kw.topic);
58063
58158
  return 0;
@@ -58431,6 +58526,11 @@ function create_gclass$1(gclass_name) {
58431
58526
  ac_auto_fit,
58432
58527
  null
58433
58528
  ],
58529
+ [
58530
+ "EV_ZOOM_SELECTION",
58531
+ ac_zoom_selection,
58532
+ null
58533
+ ],
58434
58534
  [
58435
58535
  "EV_FOCUS_TOPIC",
58436
58536
  ac_focus_topic,
@@ -58525,6 +58625,7 @@ function create_gclass$1(gclass_name) {
58525
58625
  ["EV_ZOOM_OUT", 0],
58526
58626
  ["EV_ZOOM_RESET", 0],
58527
58627
  ["EV_AUTO_FIT", 0],
58628
+ ["EV_ZOOM_SELECTION", 0],
58528
58629
  ["EV_FOCUS_TOPIC", 0],
58529
58630
  ["EV_FIND_NODES", 0],
58530
58631
  ["EV_FIND_RESULT", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
@@ -53604,6 +53604,15 @@ var ICONS = `<svg xmlns="http://www.w3.org/2000/svg">
53604
53604
  <symbol id="g6-icon-fit" viewBox="0 0 448 512">
53605
53605
  <path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"/>
53606
53606
  </symbol>
53607
+ <!-- fit to selection: the same corner brackets as the fit icon,
53608
+ around a marked object. The two sit side by side in the
53609
+ toolbar and have to read as a family, which a borrowed glyph
53610
+ would not. (No backticks in here: the sprite lives inside a
53611
+ template literal.) -->
53612
+ <symbol id="g6-icon-fit-selection" viewBox="0 0 448 512">
53613
+ <path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"/>
53614
+ <rect fill="currentColor" x="144" y="176" width="160" height="160" rx="24"/>
53615
+ </symbol>
53607
53616
  <!-- crosshairs (center) -->
53608
53617
  <symbol id="g6-icon-center" viewBox="0 0 576 512">
53609
53618
  <path fill="currentColor" d="M288-16c17.7 0 32 14.3 32 32l0 18.3c98.1 14 175.7 91.6 189.7 189.7l18.3 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-18.3 0c-14 98.1-91.6 175.7-189.7 189.7l0 18.3c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-18.3C157.9 463.7 80.3 386.1 66.3 288L48 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l18.3 0C80.3 125.9 157.9 48.3 256 34.3L256 16c0-17.7 14.3-32 32-32zM131.2 288c12.7 62.7 62.1 112.1 124.8 124.8l0-12.8c0-17.7 14.3-32 32-32s32 14.3 32 32l0 12.8c62.7-12.7 112.1-62.1 124.8-124.8L432 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l12.8 0C432.1 161.3 382.7 111.9 320 99.2l0 12.8c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-12.8C193.3 111.9 143.9 161.3 131.2 224l12.8 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-12.8 0zM288 208a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"/>
@@ -54137,6 +54146,7 @@ function update_toolbar(gobj) {
54137
54146
  if (graph_get_plugin(gobj, "toolbar")) graph.updatePlugin({ key: "toolbar" });
54138
54147
  configure_toolbar_edit(gobj);
54139
54148
  if (graph_get_plugin(gobj, "toolbar-edit")) graph.updatePlugin({ key: "toolbar-edit" });
54149
+ update_zoom_selection_button(gobj);
54140
54150
  }
54141
54151
  /************************************************************
54142
54152
  * The floating toolbars follow the theme.
@@ -54237,6 +54247,13 @@ function configure_toolbar(gobj) {
54237
54247
  title: t("actual size")
54238
54248
  }
54239
54249
  ];
54250
+ if (priv.edit_mode) items.push({
54251
+ id: "g6-icon-fit-selection",
54252
+ value: "fit-selection",
54253
+ className: "EV_ZOOM_SELECTION",
54254
+ title: t("zoom to selection"),
54255
+ disabled: true
54256
+ });
54240
54257
  if (gobj_read_bool_attr(gobj, "with_fullscreen")) {
54241
54258
  items.push({ separator: true });
54242
54259
  if (priv.is_fullscreen) items.push({
@@ -54268,6 +54285,9 @@ function configure_toolbar(gobj) {
54268
54285
  case "auto-fit":
54269
54286
  gobj_send_event(gobj, "EV_AUTO_FIT", {}, gobj);
54270
54287
  break;
54288
+ case "fit-selection":
54289
+ gobj_send_event(gobj, "EV_ZOOM_SELECTION", {}, gobj);
54290
+ break;
54271
54291
  case "center":
54272
54292
  gobj_send_event(gobj, "EV_CENTER", {}, gobj);
54273
54293
  break;
@@ -55073,6 +55093,52 @@ async function graph_fit_readable(gobj) {
55073
55093
  }
55074
55094
  }
55075
55095
  /************************************************************
55096
+ * Put the viewport on the selection: what `fit` does, for a part.
55097
+ *
55098
+ * Computed rather than delegated because `fitView()` fits the whole
55099
+ * graph and there is no subset form of it. The bounds come from the
55100
+ * elements themselves, so a node's real card size is what is
55101
+ * measured, not an assumed one; the zoom is clamped to the graph's
55102
+ * own `zoomRange`, which is the only limit that is not invented
55103
+ * here. A selection of one card filling the view is not a bug --
55104
+ * that is what zooming to it means, and it is what every editor
55105
+ * that offers the action does.
55106
+ ************************************************************/
55107
+ async function graph_fit_selection(gobj, ids) {
55108
+ let graph = gobj.priv.graph;
55109
+ if (!graph || !ids || !ids.length) return;
55110
+ let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity;
55111
+ let found = 0;
55112
+ for (let id of ids) {
55113
+ let bounds;
55114
+ try {
55115
+ bounds = graph.getElementRenderBounds(id);
55116
+ } catch (e) {
55117
+ continue;
55118
+ }
55119
+ if (!bounds || !bounds.min || !bounds.max) continue;
55120
+ minx = Math.min(minx, bounds.min[0]);
55121
+ miny = Math.min(miny, bounds.min[1]);
55122
+ maxx = Math.max(maxx, bounds.max[0]);
55123
+ maxy = Math.max(maxy, bounds.max[1]);
55124
+ found++;
55125
+ }
55126
+ if (!found) {
55127
+ log_error(`${gobj_short_name(gobj)}: the selection has no bounds to fit`);
55128
+ return;
55129
+ }
55130
+ let size = graph.getSize();
55131
+ let vw = Math.max(1, size[0] - 96);
55132
+ let vh = Math.max(1, size[1] - 96);
55133
+ let bw = Math.max(1, maxx - minx);
55134
+ let bh = Math.max(1, maxy - miny);
55135
+ let zoom = Math.min(vw / bw, vh / bh);
55136
+ let range = graph.getZoomRange() || [.2, 4];
55137
+ zoom = Math.max(range[0], Math.min(range[1], zoom));
55138
+ await graph.zoomTo(zoom);
55139
+ await graph.focusElement(ids);
55140
+ }
55141
+ /************************************************************
55076
55142
  * Focus the graph on one topic: highlight (amber 'active' state)
55077
55143
  * every node of that topic and centre the viewport on them. An
55078
55144
  * empty topic clears the highlight. If the graph is not rendered
@@ -55541,6 +55607,23 @@ function paint_selection(gobj, ids) {
55541
55607
  let next = ids || [];
55542
55608
  priv._selected_paint_ids = next;
55543
55609
  repaint_cards(gobj, /* @__PURE__ */ new Set([...prev, ...next]));
55610
+ update_zoom_selection_button(gobj);
55611
+ }
55612
+ /************************************************************
55613
+ * Zoom-to-selection can only act on a selection.
55614
+ *
55615
+ * The attribute is toggled on the element instead of re-rendering
55616
+ * the toolbar, which would rebuild its innerHTML -- and with it
55617
+ * drop the disabled state of every OTHER button -- each time a
55618
+ * card is ticked.
55619
+ ************************************************************/
55620
+ function update_zoom_selection_button(gobj) {
55621
+ let priv = gobj.priv;
55622
+ let $container = gobj_read_attr(gobj, "$container");
55623
+ if (!$container) return;
55624
+ if (!$container.querySelector(".EV_ZOOM_SELECTION")) return;
55625
+ if ((priv._selected_paint_ids || []).length > 0) enableElements($container, ".EV_ZOOM_SELECTION");
55626
+ else disableElements($container, ".EV_ZOOM_SELECTION");
55544
55627
  }
55545
55628
  /************************************************************
55546
55629
  * Select a SET of nodes: what a marquee and a shift-click do.
@@ -58045,6 +58128,18 @@ function ac_auto_fit(gobj, event, kw, src) {
58045
58128
  graph_fitview(gobj);
58046
58129
  return 0;
58047
58130
  }
58131
+ /************************************************************
58132
+ * Zoom to what is selected.
58133
+ ************************************************************/
58134
+ function ac_zoom_selection(gobj, event, kw, src) {
58135
+ let ids = gobj.priv._selected_paint_ids || [];
58136
+ if (!ids.length) {
58137
+ log_error(`${gobj_short_name(gobj)}: zoom to selection with nothing selected`);
58138
+ return -1;
58139
+ }
58140
+ graph_fit_selection(gobj, ids);
58141
+ return 0;
58142
+ }
58048
58143
  function ac_focus_topic(gobj, event, kw, src) {
58049
58144
  graph_focus_topic(gobj, kw && kw.topic);
58050
58145
  return 0;
@@ -58418,6 +58513,11 @@ function create_gclass$1(gclass_name) {
58418
58513
  ac_auto_fit,
58419
58514
  null
58420
58515
  ],
58516
+ [
58517
+ "EV_ZOOM_SELECTION",
58518
+ ac_zoom_selection,
58519
+ null
58520
+ ],
58421
58521
  [
58422
58522
  "EV_FOCUS_TOPIC",
58423
58523
  ac_focus_topic,
@@ -58512,6 +58612,7 @@ function create_gclass$1(gclass_name) {
58512
58612
  ["EV_ZOOM_OUT", 0],
58513
58613
  ["EV_ZOOM_RESET", 0],
58514
58614
  ["EV_AUTO_FIT", 0],
58615
+ ["EV_ZOOM_SELECTION", 0],
58515
58616
  ["EV_FOCUS_TOPIC", 0],
58516
58617
  ["EV_FIND_NODES", 0],
58517
58618
  ["EV_FIND_RESULT", event_flag_t.EVF_OUTPUT_EVENT],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.17.0",
3
+ "version": "7.18.0",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -912,6 +912,10 @@ function update_toolbar(gobj)
912
912
  key: 'toolbar-edit',
913
913
  });
914
914
  }
915
+
916
+ /* A re-render rebuilds the innerHTML, so a button whose enabled
917
+ * state is not part of getItems() has to be told again. */
918
+ update_zoom_selection_button(gobj);
915
919
  }
916
920
 
917
921
  /************************************************************
@@ -1031,6 +1035,20 @@ function configure_toolbar(gobj)
1031
1035
  { text: '1:1', value: 'reset', className: 'EV_ZOOM_RESET', title: t('actual size') },
1032
1036
  ];
1033
1037
 
1038
+ /* Only in edition, because that is where a selection
1039
+ * can exist: a button that is permanently disabled is
1040
+ * not a control, it is furniture. Disabled while the
1041
+ * selection is empty -- `paint_selection` is the one
1042
+ * funnel every change goes through, so it is the one
1043
+ * place that has to say so. */
1044
+ if(priv.edit_mode) {
1045
+ items.push({
1046
+ id: 'g6-icon-fit-selection', value: 'fit-selection',
1047
+ className: 'EV_ZOOM_SELECTION',
1048
+ title: t('zoom to selection'), disabled: true
1049
+ });
1050
+ }
1051
+
1034
1052
  if(gobj_read_bool_attr(gobj, "with_fullscreen")) {
1035
1053
  /* Full screen is a WINDOW control, not a camera one:
1036
1054
  * it goes in its own group. */
@@ -1062,6 +1080,9 @@ function configure_toolbar(gobj)
1062
1080
  case 'auto-fit':
1063
1081
  gobj_send_event(gobj, "EV_AUTO_FIT", {}, gobj);
1064
1082
  break;
1083
+ case 'fit-selection':
1084
+ gobj_send_event(gobj, "EV_ZOOM_SELECTION", {}, gobj);
1085
+ break;
1065
1086
  case 'center':
1066
1087
  gobj_send_event(gobj, "EV_CENTER", {}, gobj);
1067
1088
  break;
@@ -2290,6 +2311,65 @@ async function graph_fit_readable(gobj)
2290
2311
  }
2291
2312
  }
2292
2313
 
2314
+ /************************************************************
2315
+ * Put the viewport on the selection: what `fit` does, for a part.
2316
+ *
2317
+ * Computed rather than delegated because `fitView()` fits the whole
2318
+ * graph and there is no subset form of it. The bounds come from the
2319
+ * elements themselves, so a node's real card size is what is
2320
+ * measured, not an assumed one; the zoom is clamped to the graph's
2321
+ * own `zoomRange`, which is the only limit that is not invented
2322
+ * here. A selection of one card filling the view is not a bug --
2323
+ * that is what zooming to it means, and it is what every editor
2324
+ * that offers the action does.
2325
+ ************************************************************/
2326
+ async function graph_fit_selection(gobj, ids)
2327
+ {
2328
+ let priv = gobj.priv;
2329
+ let graph = priv.graph;
2330
+
2331
+ if(!graph || !ids || !ids.length) {
2332
+ return;
2333
+ }
2334
+
2335
+ let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity;
2336
+ let found = 0;
2337
+ for(let id of ids) {
2338
+ let bounds;
2339
+ try {
2340
+ bounds = graph.getElementRenderBounds(id);
2341
+ } catch(e) {
2342
+ continue; /* gone since it was selected */
2343
+ }
2344
+ if(!bounds || !bounds.min || !bounds.max) {
2345
+ continue;
2346
+ }
2347
+ minx = Math.min(minx, bounds.min[0]);
2348
+ miny = Math.min(miny, bounds.min[1]);
2349
+ maxx = Math.max(maxx, bounds.max[0]);
2350
+ maxy = Math.max(maxy, bounds.max[1]);
2351
+ found++;
2352
+ }
2353
+ if(!found) {
2354
+ log_error(`${gobj_short_name(gobj)}: the selection has no bounds to fit`);
2355
+ return;
2356
+ }
2357
+
2358
+ const MARGIN = 48;
2359
+ let size = graph.getSize();
2360
+ let vw = Math.max(1, size[0] - MARGIN * 2);
2361
+ let vh = Math.max(1, size[1] - MARGIN * 2);
2362
+ let bw = Math.max(1, maxx - minx);
2363
+ let bh = Math.max(1, maxy - miny);
2364
+
2365
+ let zoom = Math.min(vw / bw, vh / bh);
2366
+ let range = graph.getZoomRange() || [0.2, 4];
2367
+ zoom = Math.max(range[0], Math.min(range[1], zoom));
2368
+
2369
+ await graph.zoomTo(zoom);
2370
+ await graph.focusElement(ids);
2371
+ }
2372
+
2293
2373
  /************************************************************
2294
2374
  * Focus the graph on one topic: highlight (amber 'active' state)
2295
2375
  * every node of that topic and centre the viewport on them. An
@@ -3049,6 +3129,36 @@ function paint_selection(gobj, ids)
3049
3129
  priv._selected_paint_ids = next;
3050
3130
 
3051
3131
  repaint_cards(gobj, new Set([...prev, ...next]));
3132
+ update_zoom_selection_button(gobj);
3133
+ }
3134
+
3135
+ /************************************************************
3136
+ * Zoom-to-selection can only act on a selection.
3137
+ *
3138
+ * The attribute is toggled on the element instead of re-rendering
3139
+ * the toolbar, which would rebuild its innerHTML -- and with it
3140
+ * drop the disabled state of every OTHER button -- each time a
3141
+ * card is ticked.
3142
+ ************************************************************/
3143
+ function update_zoom_selection_button(gobj)
3144
+ {
3145
+ let priv = gobj.priv;
3146
+ let $container = gobj_read_attr(gobj, "$container");
3147
+
3148
+ if(!$container) {
3149
+ return;
3150
+ }
3151
+
3152
+ let $btn = $container.querySelector(".EV_ZOOM_SELECTION");
3153
+ if(!$btn) {
3154
+ return; /* no toolbar, or not in edition: nothing to enable */
3155
+ }
3156
+
3157
+ if((priv._selected_paint_ids || []).length > 0) {
3158
+ enableElements($container, ".EV_ZOOM_SELECTION");
3159
+ } else {
3160
+ disableElements($container, ".EV_ZOOM_SELECTION");
3161
+ }
3052
3162
  }
3053
3163
 
3054
3164
  /************************************************************
@@ -6774,6 +6884,25 @@ function ac_auto_fit(gobj, event, kw, src)
6774
6884
  return 0;
6775
6885
  }
6776
6886
 
6887
+ /************************************************************
6888
+ * Zoom to what is selected.
6889
+ ************************************************************/
6890
+ function ac_zoom_selection(gobj, event, kw, src)
6891
+ {
6892
+ let ids = gobj.priv._selected_paint_ids || [];
6893
+
6894
+ if(!ids.length) {
6895
+ /* The button is disabled without a selection, so getting here
6896
+ * means the event came from somewhere else. */
6897
+ log_error(`${gobj_short_name(gobj)}: zoom to selection with nothing selected`);
6898
+ return -1;
6899
+ }
6900
+
6901
+ graph_fit_selection(gobj, ids);
6902
+
6903
+ return 0;
6904
+ }
6905
+
6777
6906
  function ac_focus_topic(gobj, event, kw, src)
6778
6907
  {
6779
6908
  graph_focus_topic(gobj, kw && kw.topic);
@@ -7232,6 +7361,7 @@ function create_gclass(gclass_name)
7232
7361
  ["EV_ZOOM_OUT", ac_zoom_out, null],
7233
7362
  ["EV_ZOOM_RESET", ac_zoom_reset, null],
7234
7363
  ["EV_AUTO_FIT", ac_auto_fit, null],
7364
+ ["EV_ZOOM_SELECTION", ac_zoom_selection, null],
7235
7365
  ["EV_FOCUS_TOPIC", ac_focus_topic, null],
7236
7366
  ["EV_FIND_NODES", ac_find_nodes, null],
7237
7367
  ["EV_CENTER", ac_center, null],
@@ -7278,6 +7408,7 @@ function create_gclass(gclass_name)
7278
7408
  ["EV_ZOOM_OUT", 0],
7279
7409
  ["EV_ZOOM_RESET", 0],
7280
7410
  ["EV_AUTO_FIT", 0],
7411
+ ["EV_ZOOM_SELECTION", 0],
7281
7412
  ["EV_FOCUS_TOPIC", 0],
7282
7413
  ["EV_FIND_NODES", 0],
7283
7414
  ["EV_FIND_RESULT", event_flag_t.EVF_OUTPUT_EVENT],
package/src/lib_icons.js CHANGED
@@ -52,6 +52,15 @@ const ICONS = `<svg xmlns="http://www.w3.org/2000/svg">
52
52
  <symbol id="g6-icon-fit" viewBox="0 0 448 512">
53
53
  <path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"/>
54
54
  </symbol>
55
+ <!-- fit to selection: the same corner brackets as the fit icon,
56
+ around a marked object. The two sit side by side in the
57
+ toolbar and have to read as a family, which a borrowed glyph
58
+ would not. (No backticks in here: the sprite lives inside a
59
+ template literal.) -->
60
+ <symbol id="g6-icon-fit-selection" viewBox="0 0 448 512">
61
+ <path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"/>
62
+ <rect fill="currentColor" x="144" y="176" width="160" height="160" rx="24"/>
63
+ </symbol>
55
64
  <!-- crosshairs (center) -->
56
65
  <symbol id="g6-icon-center" viewBox="0 0 576 512">
57
66
  <path fill="currentColor" d="M288-16c17.7 0 32 14.3 32 32l0 18.3c98.1 14 175.7 91.6 189.7 189.7l18.3 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-18.3 0c-14 98.1-91.6 175.7-189.7 189.7l0 18.3c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-18.3C157.9 463.7 80.3 386.1 66.3 288L48 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l18.3 0C80.3 125.9 157.9 48.3 256 34.3L256 16c0-17.7 14.3-32 32-32zM131.2 288c12.7 62.7 62.1 112.1 124.8 124.8l0-12.8c0-17.7 14.3-32 32-32s32 14.3 32 32l0 12.8c62.7-12.7 112.1-62.1 124.8-124.8L432 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l12.8 0C432.1 161.3 382.7 111.9 320 99.2l0 12.8c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-12.8C193.3 111.9 143.9 161.3 131.2 224l12.8 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-12.8 0zM288 208a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"/>