@ixfx/components 0.11.4 → 0.11.5
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/bundle/index.d.ts +442 -120
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +2230 -538
- package/bundle/index.js.map +1 -1
- package/dist/ac-text.d.ts +1 -1
- package/dist/{ac-token-TCLAZeVg.d.ts → ac-token--_vCHXWw.d.ts} +2 -2
- package/dist/{ac-token-TCLAZeVg.d.ts.map → ac-token--_vCHXWw.d.ts.map} +1 -1
- package/dist/ac-token.d.ts +1 -1
- package/dist/{button-CTL45aBl.d.ts → button-BYuf72aR.d.ts} +2 -2
- package/dist/{button-CTL45aBl.d.ts.map → button-BYuf72aR.d.ts.map} +1 -1
- package/dist/button.d.ts +1 -1
- package/dist/colour-picker.d.ts +1 -1
- package/dist/crumbs.d.ts +3 -3
- package/dist/{data-grid-CfiaBqTA.d.ts → data-grid-CPsJBnL4.d.ts} +48 -6
- package/dist/data-grid-CPsJBnL4.d.ts.map +1 -0
- package/dist/data-grid.d.ts +3 -3
- package/dist/data-grid.js +66 -2
- package/dist/data-grid.js.map +1 -1
- package/dist/{detail-list-DxQZkK-C.d.ts → detail-list-CmdCxddo.d.ts} +22 -5
- package/dist/detail-list-CmdCxddo.d.ts.map +1 -0
- package/dist/detail-list.d.ts +3 -3
- package/dist/detail-list.js +67 -4
- package/dist/detail-list.js.map +1 -1
- package/dist/{grid-list-k89JpiNy.js → grid-list-3fGhdaFA.js} +92 -7
- package/dist/grid-list-3fGhdaFA.js.map +1 -0
- package/dist/{grid-list-ChgYpUUq.d.ts → grid-list-D6AfWfwn.d.ts} +27 -4
- package/dist/grid-list-D6AfWfwn.d.ts.map +1 -0
- package/dist/grid-list.d.ts +2 -2
- package/dist/grid-list.js +1 -1
- package/dist/{grouped-item-lister-Bhyo-dkH.d.ts → grouped-item-lister-DETte0Dh.d.ts} +2 -2
- package/dist/{grouped-item-lister-Bhyo-dkH.d.ts.map → grouped-item-lister-DETte0Dh.d.ts.map} +1 -1
- package/dist/grouped-item-lister.d.ts +1 -1
- package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +2 -2
- package/dist/icon-CDGprSdE.d.ts.map +1 -0
- package/dist/icons.d.ts +2 -2
- package/dist/incr-search.d.ts +1 -1
- package/dist/{index-CHbuPyLL.d.ts → index-CMsmZ6Or.d.ts} +4 -4
- package/dist/{index-CHbuPyLL.d.ts.map → index-CMsmZ6Or.d.ts.map} +1 -1
- package/dist/{index-garxxqgV.d.ts → index-CYndvTWH.d.ts} +2 -2
- package/dist/{index-garxxqgV.d.ts.map → index-CYndvTWH.d.ts.map} +1 -1
- package/dist/{index-CDllxjVG.d.ts → index-CnyoNA6m.d.ts} +4 -4
- package/dist/{index-CDllxjVG.d.ts.map → index-CnyoNA6m.d.ts.map} +1 -1
- package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +2 -2
- package/dist/{index-BUB5SICW.d.ts.map → index-CoqtMSd9.d.ts.map} +1 -1
- package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +2 -2
- package/dist/{index-CJ5fshD3.d.ts.map → index-UvnoZUAo.d.ts.map} +1 -1
- package/dist/{index-BOUaDIW-.d.ts → index-tq6f8007.d.ts} +2 -2
- package/dist/{index-BOUaDIW-.d.ts.map → index-tq6f8007.d.ts.map} +1 -1
- package/dist/index.d.ts +337 -140
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2036 -644
- package/dist/index.js.map +1 -1
- package/dist/item-appearance-BjcMyauw.js +31 -0
- package/dist/item-appearance-BjcMyauw.js.map +1 -0
- package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +2 -2
- package/dist/{labelled-input-base-Dp_9KP0G.d.ts.map → labelled-input-base-DiTme1JO.d.ts.map} +1 -1
- package/dist/labelled-radial-input.d.ts +2 -2
- package/dist/labelled-radial-input.d.ts.map +1 -1
- package/dist/labelled-range-input.d.ts +1 -1
- package/dist/labelled-slider-input.d.ts +1 -1
- package/dist/{list-native-drag-controller-B9reY4uM.d.ts → list-native-drag-controller-CllsHLxw.d.ts} +2 -2
- package/dist/{list-native-drag-controller-B9reY4uM.d.ts.map → list-native-drag-controller-CllsHLxw.d.ts.map} +1 -1
- package/dist/{menu-item-DlxAb0Xr.d.ts → menu-item-DT7bKAIw.d.ts} +2 -2
- package/dist/{menu-item-DlxAb0Xr.d.ts.map → menu-item-DT7bKAIw.d.ts.map} +1 -1
- package/dist/menu.d.ts +2 -2
- package/dist/miller.d.ts +3 -3
- package/dist/miller.d.ts.map +1 -1
- package/dist/miller.js +18 -2
- package/dist/miller.js.map +1 -1
- package/dist/panel.d.ts +1 -1
- package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-JK3xWVX2.d.ts} +2 -2
- package/dist/{split-layout-C5SYbDrq.d.ts.map → split-layout-JK3xWVX2.d.ts.map} +1 -1
- package/dist/split-layout.d.ts +1 -1
- package/dist/{tab-list-v8mKX7Fu.d.ts → tab-list-pqfKaVAE.d.ts} +2 -2
- package/dist/{tab-list-v8mKX7Fu.d.ts.map → tab-list-pqfKaVAE.d.ts.map} +1 -1
- package/dist/tabs.d.ts +1 -1
- package/dist/{titlebar-B5zSTZnF.d.ts → titlebar-Tqu-VvMU.d.ts} +2 -2
- package/dist/{titlebar-B5zSTZnF.d.ts.map → titlebar-Tqu-VvMU.d.ts.map} +1 -1
- package/dist/titlebar.d.ts +1 -1
- package/dist/{tree-component-CMGeqyjZ.d.ts → tree-component-DukLqesg.d.ts} +2 -2
- package/dist/{tree-component-CMGeqyjZ.d.ts.map → tree-component-DukLqesg.d.ts.map} +1 -1
- package/dist/tree.d.ts +4 -4
- package/dist/{types-3Jfcaxvo.d.ts → types-D-kYF16b.d.ts} +21 -1
- package/dist/types-D-kYF16b.d.ts.map +1 -0
- package/dist/{vertical-list-R25WqAVO.d.ts → vertical-list-DTp-UIUE.d.ts} +26 -3
- package/dist/vertical-list-DTp-UIUE.d.ts.map +1 -0
- package/dist/vertical-list.d.ts +2 -2
- package/dist/vertical-list.js +53 -4
- package/dist/vertical-list.js.map +1 -1
- package/docs-user/data-grid.md +51 -1
- package/docs-user/detail-list.md +46 -0
- package/docs-user/grid-list.md +49 -2
- package/docs-user/hyperbolic-tree.md +98 -18
- package/docs-user/index.json +2 -2
- package/docs-user/miller.md +52 -7
- package/docs-user/vertical-list.md +63 -0
- package/package.json +1 -1
- package/dist/data-grid-CfiaBqTA.d.ts.map +0 -1
- package/dist/detail-list-DxQZkK-C.d.ts.map +0 -1
- package/dist/grid-list-ChgYpUUq.d.ts.map +0 -1
- package/dist/grid-list-k89JpiNy.js.map +0 -1
- package/dist/icon-DR_3rSyZ.d.ts.map +0 -1
- package/dist/types-3Jfcaxvo.d.ts.map +0 -1
- package/dist/vertical-list-R25WqAVO.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ import { t as resolveAxisStep } from "./keyboard-B5xD7Fz-.js";
|
|
|
42
42
|
import { t as WheelNudgeMixin } from "./wheel-nudge-D5rmht3v.js";
|
|
43
43
|
import { n as EditableNumber, t as LabelledSliderInput } from "./labelled-slider-input-FdCRi6st.js";
|
|
44
44
|
import { t as FormElement } from "./form-CsXJX15p.js";
|
|
45
|
-
import { t as GridListElement } from "./grid-list-
|
|
45
|
+
import { t as GridListElement } from "./grid-list-3fGhdaFA.js";
|
|
46
46
|
import { PanelElement } from "./panel.js";
|
|
47
47
|
import { GroupedItemListerElement } from "./grouped-item-lister.js";
|
|
48
48
|
import { VerticalListElement } from "./vertical-list.js";
|
|
@@ -1497,6 +1497,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
1497
1497
|
this.path = ``;
|
|
1498
1498
|
this.separator = `/`;
|
|
1499
1499
|
this.pathRoot = ``;
|
|
1500
|
+
this.hintPrevious = false;
|
|
1500
1501
|
this._openIdx = null;
|
|
1501
1502
|
this._editing = false;
|
|
1502
1503
|
this._cachedSiblings = /* @__PURE__ */ new Map();
|
|
@@ -1508,6 +1509,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
1508
1509
|
this._openCumPath = null;
|
|
1509
1510
|
this._dragCumPath = null;
|
|
1510
1511
|
this._dropCumPath = null;
|
|
1512
|
+
this._hintSegments = null;
|
|
1511
1513
|
this.#uid = Math.random().toString(36).slice(2, 8);
|
|
1512
1514
|
this.#clickSuppressUntil = 0;
|
|
1513
1515
|
this.#onPopoverToggle = (e) => {
|
|
@@ -1643,6 +1645,19 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
1643
1645
|
#clicksSuppressed() {
|
|
1644
1646
|
return Date.now() < this.#clickSuppressUntil || this._dragCumPath !== null;
|
|
1645
1647
|
}
|
|
1648
|
+
#isSegmentPrefix(prefix, full) {
|
|
1649
|
+
return prefix.length <= full.length && prefix.every((seg, i) => seg === full[i]);
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Called when a real segment click shortens the path. Keeps the deepest
|
|
1653
|
+
* known branch: only overwrites `_hintSegments` when the current hint no
|
|
1654
|
+
* longer covers where we're navigating to.
|
|
1655
|
+
*/
|
|
1656
|
+
#recordHintOnShorten(oldSegments, newSegments) {
|
|
1657
|
+
if (!this.hintPrevious) return;
|
|
1658
|
+
const hint = this._hintSegments;
|
|
1659
|
+
if (!(hint !== null && this.#isSegmentPrefix(newSegments, hint))) this._hintSegments = oldSegments;
|
|
1660
|
+
}
|
|
1646
1661
|
#resolveSegmentIcon(fullPathSegments, index) {
|
|
1647
1662
|
const direct = this.segmentIconLookup?.(fullPathSegments, index);
|
|
1648
1663
|
if (typeof direct === `string` && direct.length > 0) return direct;
|
|
@@ -1788,12 +1803,63 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
1788
1803
|
composed: true
|
|
1789
1804
|
}));
|
|
1790
1805
|
}
|
|
1806
|
+
/**
|
|
1807
|
+
* The current leaf segment (deepest visible segment), used as the drop target
|
|
1808
|
+
* when a drag is released over blank space rather than a specific `.seg`.
|
|
1809
|
+
*/
|
|
1810
|
+
#leafSegment() {
|
|
1811
|
+
const segments = this.#getSegments();
|
|
1812
|
+
if (segments.length > 0) {
|
|
1813
|
+
const index = segments.length - 1;
|
|
1814
|
+
return {
|
|
1815
|
+
cumPath: this.#getCumulativePath(index, false),
|
|
1816
|
+
fullPathSegments: this.#resolveFullPathSegments(index, false)
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
const rootSegments = this.#getRootSegments();
|
|
1820
|
+
if (rootSegments.length > 0) {
|
|
1821
|
+
const index = rootSegments.length - 1;
|
|
1822
|
+
return {
|
|
1823
|
+
cumPath: this.#getCumulativePath(index, true),
|
|
1824
|
+
fullPathSegments: this.#resolveFullPathSegments(index, true)
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Is `event` already being handled by a `.seg`'s own drag handlers? Virtual
|
|
1830
|
+
* (hint) segments don't have drag handlers of their own, so a drag released
|
|
1831
|
+
* over one falls through to the host-level fallback (the real leaf).
|
|
1832
|
+
*/
|
|
1833
|
+
#eventTargetsSegment(event) {
|
|
1834
|
+
return Boolean(event.target?.closest?.(`.seg:not(.seg-virtual)`));
|
|
1835
|
+
}
|
|
1836
|
+
#onHostDragOver(event) {
|
|
1837
|
+
if (!this.parseExternalDrop || !event.dataTransfer) return;
|
|
1838
|
+
if (this.#eventTargetsSegment(event)) return;
|
|
1839
|
+
const leaf = this.#leafSegment();
|
|
1840
|
+
if (!leaf) return;
|
|
1841
|
+
event.preventDefault();
|
|
1842
|
+
if (this._dropCumPath !== leaf.cumPath) this._dropCumPath = leaf.cumPath;
|
|
1843
|
+
}
|
|
1844
|
+
#onHostDragLeave(event) {
|
|
1845
|
+
if (!this.parseExternalDrop || this.#eventTargetsSegment(event)) return;
|
|
1846
|
+
const leaf = this.#leafSegment();
|
|
1847
|
+
if (leaf && this._dropCumPath === leaf.cumPath) this._dropCumPath = null;
|
|
1848
|
+
}
|
|
1849
|
+
#onHostDrop(event) {
|
|
1850
|
+
if (!this.parseExternalDrop || this.#eventTargetsSegment(event)) return;
|
|
1851
|
+
const leaf = this.#leafSegment();
|
|
1852
|
+
if (!leaf) return;
|
|
1853
|
+
this.#onSegDrop(leaf.fullPathSegments, event);
|
|
1854
|
+
}
|
|
1791
1855
|
#onLabelClick(index, event) {
|
|
1792
1856
|
if (this.#clicksSuppressed()) return;
|
|
1793
1857
|
event.preventDefault();
|
|
1794
1858
|
event.stopPropagation();
|
|
1795
1859
|
this.#closeDropdown();
|
|
1796
|
-
const
|
|
1860
|
+
const segments = this.#getSegments();
|
|
1861
|
+
const truncatedSegments = segments.slice(0, index + 1);
|
|
1862
|
+
this.#recordHintOnShorten(segments, truncatedSegments);
|
|
1797
1863
|
const hadLeading = this.path.startsWith(this.separator);
|
|
1798
1864
|
const newPath = truncatedSegments.join(this.separator);
|
|
1799
1865
|
this.path = hadLeading ? this.separator + newPath : newPath;
|
|
@@ -1811,6 +1877,31 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
1811
1877
|
composed: true
|
|
1812
1878
|
}));
|
|
1813
1879
|
}
|
|
1880
|
+
#onVirtualSegmentClick(hintIndex, event) {
|
|
1881
|
+
if (this.#clicksSuppressed()) return;
|
|
1882
|
+
event.preventDefault();
|
|
1883
|
+
event.stopPropagation();
|
|
1884
|
+
this.#closeDropdown();
|
|
1885
|
+
const hint = this._hintSegments;
|
|
1886
|
+
if (!hint) return;
|
|
1887
|
+
const truncatedSegments = hint.slice(0, hintIndex + 1);
|
|
1888
|
+
const hadLeading = this.path.startsWith(this.separator);
|
|
1889
|
+
const newPath = truncatedSegments.join(this.separator);
|
|
1890
|
+
this.path = hadLeading ? this.separator + newPath : newPath;
|
|
1891
|
+
this._cachedSiblings = /* @__PURE__ */ new Map();
|
|
1892
|
+
this._leafSegmentIndex = null;
|
|
1893
|
+
const parentPath = truncatedSegments.slice(0, -1).join(this.separator);
|
|
1894
|
+
this.dispatchEvent(new CustomEvent(`navigate`, {
|
|
1895
|
+
detail: {
|
|
1896
|
+
index: hintIndex,
|
|
1897
|
+
label: truncatedSegments[hintIndex],
|
|
1898
|
+
parentPath,
|
|
1899
|
+
fullPath: newPath
|
|
1900
|
+
},
|
|
1901
|
+
bubbles: true,
|
|
1902
|
+
composed: true
|
|
1903
|
+
}));
|
|
1904
|
+
}
|
|
1814
1905
|
#selectSibling(index, segment) {
|
|
1815
1906
|
const cumPath = this._openCumPath;
|
|
1816
1907
|
this.#closeDropdown();
|
|
@@ -1886,7 +1977,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
1886
1977
|
@drop=${(e) => this.#onSegDrop(fullPathSegments, e)}>
|
|
1887
1978
|
<label title=${label}
|
|
1888
1979
|
part="label"
|
|
1889
|
-
|
|
1980
|
+
draggable=${draggable ? `true` : `false`}
|
|
1890
1981
|
@click=${(e) => this.#onLabelClick(index, e)}
|
|
1891
1982
|
@dragstart=${(e) => this.#onLabelDragStart(fullPathSegments, cumPath, e)}
|
|
1892
1983
|
@dragend=${() => this.#onLabelDragEnd()}>
|
|
@@ -1903,6 +1994,26 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
1903
1994
|
</div>
|
|
1904
1995
|
` : nothing}
|
|
1905
1996
|
</div>
|
|
1997
|
+
`;
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* A translucent, click-to-restore segment beyond the current path, shown
|
|
2001
|
+
* while `hintPrevious` is enabled and the current path is a prefix of the
|
|
2002
|
+
* deepest path visited. Doesn't participate in drag'n'drop and isn't
|
|
2003
|
+
* keyboard-focusable.
|
|
2004
|
+
*/
|
|
2005
|
+
#renderVirtualSegment(label, hintIndex) {
|
|
2006
|
+
const fullPathSegments = [...this.#getRootSegments(), ...(this._hintSegments ?? []).slice(0, hintIndex + 1)];
|
|
2007
|
+
const icon = this.#resolveSegmentIcon(fullPathSegments, fullPathSegments.length - 1);
|
|
2008
|
+
return html`
|
|
2009
|
+
<div class="seg seg-virtual" part="segment virtual">
|
|
2010
|
+
<label title=${label}
|
|
2011
|
+
part="label"
|
|
2012
|
+
@click=${(e) => this.#onVirtualSegmentClick(hintIndex, e)}>
|
|
2013
|
+
${icon ? html`<ixfx-icon class="crumb-icon segment-icon" name=${icon}></ixfx-icon>` : nothing}
|
|
2014
|
+
${label}
|
|
2015
|
+
</label>
|
|
2016
|
+
</div>
|
|
1906
2017
|
`;
|
|
1907
2018
|
}
|
|
1908
2019
|
#renderRootSegment(label, index) {
|
|
@@ -1924,7 +2035,7 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
1924
2035
|
@drop=${(e) => this.#onSegDrop(fullPathSegments, e)}>
|
|
1925
2036
|
<label title=${label}
|
|
1926
2037
|
part="label"
|
|
1927
|
-
|
|
2038
|
+
draggable=${draggable ? `true` : `false`}
|
|
1928
2039
|
@click=${(e) => this.#onLabelClick(index, e)}
|
|
1929
2040
|
@dragstart=${(e) => this.#onLabelDragStart(fullPathSegments, cumPath, e)}
|
|
1930
2041
|
@dragend=${() => this.#onLabelDragEnd()}>${icon ? html`<ixfx-icon class="crumb-icon segment-icon" name=${icon}></ixfx-icon>` : nothing}${label}</label>
|
|
@@ -2030,6 +2141,8 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
2030
2141
|
#onHostKeyDown;
|
|
2031
2142
|
render() {
|
|
2032
2143
|
const segments = this.#getSegments();
|
|
2144
|
+
const hintSegments = this.hintPrevious ? this._hintSegments : null;
|
|
2145
|
+
const virtualSegments = hintSegments !== null && hintSegments.length > segments.length && this.#isSegmentPrefix(segments, hintSegments) ? hintSegments.slice(segments.length) : [];
|
|
2033
2146
|
if (this._editing) {
|
|
2034
2147
|
const editValue = this.path;
|
|
2035
2148
|
return html`
|
|
@@ -2052,10 +2165,15 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
2052
2165
|
${this.separator}
|
|
2053
2166
|
</div>` : nothing;
|
|
2054
2167
|
return html`
|
|
2055
|
-
<div class="crumbs scroll-fade-x" tabindex="0"
|
|
2168
|
+
<div class="crumbs scroll-fade-x" tabindex="0"
|
|
2169
|
+
@keydown=${this.#onHostKeyDown}
|
|
2170
|
+
@dragover=${(e) => this.#onHostDragOver(e)}
|
|
2171
|
+
@dragleave=${(e) => this.#onHostDragLeave(e)}
|
|
2172
|
+
@drop=${(e) => this.#onHostDrop(e)}>
|
|
2056
2173
|
${rootSegments.length > 0 ? rootSegments.map((label, i) => this.#renderRootSegment(label, i)) : nothing}
|
|
2057
2174
|
${leadingCaret}
|
|
2058
2175
|
${segments.map((label, i) => this.#renderSegment(label, i, i === segments.length - 1))}
|
|
2176
|
+
${virtualSegments.map((label, i) => this.#renderVirtualSegment(label, segments.length + i))}
|
|
2059
2177
|
<div class="trailing" @click=${this.#onTrailingClick}></div>
|
|
2060
2178
|
</div>
|
|
2061
2179
|
${this._openIdx !== null ? html`
|
|
@@ -2082,6 +2200,8 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
2082
2200
|
--padding-m: var(--space-s);
|
|
2083
2201
|
--hover-bg: var(--surface-5);
|
|
2084
2202
|
--hover-fg: var(--surface-5-text);
|
|
2203
|
+
--crumb-hint-opacity: 0.35;
|
|
2204
|
+
--crumb-hint-hover-opacity: 0.6;
|
|
2085
2205
|
|
|
2086
2206
|
display: flex;
|
|
2087
2207
|
width: 100%;
|
|
@@ -2193,6 +2313,14 @@ let CrumbPathElement = class CrumbPathElement extends LitElement {
|
|
|
2193
2313
|
background: var(--crumb-drop-into-bg, color-mix(in srgb, var(--accent) 14%, transparent));
|
|
2194
2314
|
}
|
|
2195
2315
|
|
|
2316
|
+
.seg-virtual {
|
|
2317
|
+
opacity: var(--crumb-hint-opacity);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
.seg-virtual:hover {
|
|
2321
|
+
opacity: var(--crumb-hint-hover-opacity);
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2196
2324
|
.crumb-icon {
|
|
2197
2325
|
width: 1em;
|
|
2198
2326
|
height: 1em;
|
|
@@ -2277,6 +2405,10 @@ __decorate([property({ type: Object })], CrumbPathElement.prototype, "siblingLoo
|
|
|
2277
2405
|
__decorate([property({ type: Object })], CrumbPathElement.prototype, "segmentIconLookup", void 0);
|
|
2278
2406
|
__decorate([property({ type: Object })], CrumbPathElement.prototype, "segmentDragLookup", void 0);
|
|
2279
2407
|
__decorate([property({ type: Object })], CrumbPathElement.prototype, "parseExternalDrop", void 0);
|
|
2408
|
+
__decorate([property({
|
|
2409
|
+
type: Boolean,
|
|
2410
|
+
attribute: `hint-previous`
|
|
2411
|
+
})], CrumbPathElement.prototype, "hintPrevious", void 0);
|
|
2280
2412
|
__decorate([state()], CrumbPathElement.prototype, "_openIdx", void 0);
|
|
2281
2413
|
__decorate([state()], CrumbPathElement.prototype, "_editing", void 0);
|
|
2282
2414
|
__decorate([state()], CrumbPathElement.prototype, "_cachedSiblings", void 0);
|
|
@@ -2288,6 +2420,7 @@ __decorate([state()], CrumbPathElement.prototype, "_editStartIndex", void 0);
|
|
|
2288
2420
|
__decorate([state()], CrumbPathElement.prototype, "_openCumPath", void 0);
|
|
2289
2421
|
__decorate([state()], CrumbPathElement.prototype, "_dragCumPath", void 0);
|
|
2290
2422
|
__decorate([state()], CrumbPathElement.prototype, "_dropCumPath", void 0);
|
|
2423
|
+
__decorate([state()], CrumbPathElement.prototype, "_hintSegments", void 0);
|
|
2291
2424
|
CrumbPathElement = __decorate([safeCustomElement(`ixfx-crumb-path`)], CrumbPathElement);
|
|
2292
2425
|
//#endregion
|
|
2293
2426
|
//#region src/data-table/data-table.ts
|
|
@@ -5721,6 +5854,90 @@ function createVerticalListAdapter(renderItem, options) {
|
|
|
5721
5854
|
});
|
|
5722
5855
|
return adapter;
|
|
5723
5856
|
}
|
|
5857
|
+
/**
|
|
5858
|
+
* Whether `n` deserves a preview: it has children and at least one of them
|
|
5859
|
+
* isn't legible right now — laid out without a label, or not laid out at
|
|
5860
|
+
* all (outside the `maxRadius` window).
|
|
5861
|
+
*/
|
|
5862
|
+
function needsChildPreview(n, visible, labelIds) {
|
|
5863
|
+
const children = n.node.children ?? [];
|
|
5864
|
+
if (children.length === 0) return false;
|
|
5865
|
+
const shown = visible.filter((v) => v.parentId === n.id);
|
|
5866
|
+
return shown.length < children.length || shown.some((v) => !labelIds.has(v.id));
|
|
5867
|
+
}
|
|
5868
|
+
/** Where node position `p` (disk units) lands on screen, in CSS px from the host's top-left. */
|
|
5869
|
+
function diskToHostPx(p, zoom, width, height) {
|
|
5870
|
+
const pxPerUnit = Math.min(width, height) / 2.1;
|
|
5871
|
+
return {
|
|
5872
|
+
x: width / 2 + (zoom.offset.x + zoom.zoom * p.x) * pxPerUnit,
|
|
5873
|
+
y: height / 2 + (zoom.offset.y + zoom.zoom * p.y) * pxPerUnit
|
|
5874
|
+
};
|
|
5875
|
+
}
|
|
5876
|
+
function renderChildPreview(preview, handlers) {
|
|
5877
|
+
if (!preview) return nothing;
|
|
5878
|
+
const children = preview.node.children ?? [];
|
|
5879
|
+
const listed = children.slice(0, 12);
|
|
5880
|
+
const more = children.length - listed.length;
|
|
5881
|
+
return html`
|
|
5882
|
+
<div
|
|
5883
|
+
class="child-preview ${preview.above ? `above` : `below`}"
|
|
5884
|
+
part="child-preview"
|
|
5885
|
+
role="dialog"
|
|
5886
|
+
aria-label=${`Children of ${preview.node.item.label}`}
|
|
5887
|
+
style="left: ${preview.anchor.x}px; top: ${preview.anchor.y}px"
|
|
5888
|
+
@pointerenter=${handlers.onPointerEnter}
|
|
5889
|
+
@pointerleave=${handlers.onPointerLeave}
|
|
5890
|
+
@pointerdown=${(e) => e.stopPropagation()}
|
|
5891
|
+
>
|
|
5892
|
+
<div class="child-preview-title">${preview.node.item.label}</div>
|
|
5893
|
+
<ul>
|
|
5894
|
+
${listed.map((child) => html`<li>
|
|
5895
|
+
<button type="button" tabindex="-1" @click=${() => handlers.onPick(child)}>${child.item.label}</button>
|
|
5896
|
+
</li>`)}
|
|
5897
|
+
</ul>
|
|
5898
|
+
${more > 0 ? html`<div class="child-preview-more">+${more} more</div>` : nothing}
|
|
5899
|
+
</div>
|
|
5900
|
+
`;
|
|
5901
|
+
}
|
|
5902
|
+
/**
|
|
5903
|
+
* Show/hide timing for the preview: shows at once for a qualifying node;
|
|
5904
|
+
* otherwise hides after a short grace period, unless the pointer is over the
|
|
5905
|
+
* popover itself — so it can travel from the node onto the list.
|
|
5906
|
+
*/
|
|
5907
|
+
function createChildPreviewTiming(host) {
|
|
5908
|
+
let hovered = false;
|
|
5909
|
+
let timer;
|
|
5910
|
+
const update = (id) => {
|
|
5911
|
+
clearTimeout(timer);
|
|
5912
|
+
const candidate = host.candidate(id);
|
|
5913
|
+
if (candidate !== void 0) {
|
|
5914
|
+
host.setPreviewId(candidate);
|
|
5915
|
+
return;
|
|
5916
|
+
}
|
|
5917
|
+
timer = setTimeout(() => {
|
|
5918
|
+
if (!hovered) host.setPreviewId(void 0);
|
|
5919
|
+
}, 250);
|
|
5920
|
+
};
|
|
5921
|
+
return {
|
|
5922
|
+
update,
|
|
5923
|
+
pointerEnter() {
|
|
5924
|
+
hovered = true;
|
|
5925
|
+
clearTimeout(timer);
|
|
5926
|
+
},
|
|
5927
|
+
pointerLeave(tickledId) {
|
|
5928
|
+
hovered = false;
|
|
5929
|
+
update(tickledId);
|
|
5930
|
+
},
|
|
5931
|
+
hide() {
|
|
5932
|
+
hovered = false;
|
|
5933
|
+
clearTimeout(timer);
|
|
5934
|
+
host.setPreviewId(void 0);
|
|
5935
|
+
},
|
|
5936
|
+
dispose() {
|
|
5937
|
+
clearTimeout(timer);
|
|
5938
|
+
}
|
|
5939
|
+
};
|
|
5940
|
+
}
|
|
5724
5941
|
//#endregion
|
|
5725
5942
|
//#region src/hyperbolic-tree/poincare-math.ts
|
|
5726
5943
|
const EPSILON = 1e-9;
|
|
@@ -5825,237 +6042,954 @@ function geodesicInterpolate(a, b, t) {
|
|
|
5825
6042
|
if (localNorm < EPSILON) return a;
|
|
5826
6043
|
return mobiusFromOrigin(complexScale(complexScale(bLocal, 1 / localNorm), euclideanRadiusForHyperbolicDistance(hyperbolicDistanceForEuclideanRadius(localNorm) * t)), a);
|
|
5827
6044
|
}
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
if (hit) return hit;
|
|
6045
|
+
const identityIsometry = {
|
|
6046
|
+
a: {
|
|
6047
|
+
x: 1,
|
|
6048
|
+
y: 0
|
|
6049
|
+
},
|
|
6050
|
+
b: {
|
|
6051
|
+
x: 0,
|
|
6052
|
+
y: 0
|
|
5837
6053
|
}
|
|
6054
|
+
};
|
|
6055
|
+
/** Apply `m` to `z`. */
|
|
6056
|
+
function applyIsometry(m, z) {
|
|
6057
|
+
return clampToDisk(complexDiv(complexAdd(complexMul(m.a, z), m.b), complexAdd(complexMul(complexConjugate(m.b), z), complexConjugate(m.a))));
|
|
6058
|
+
}
|
|
6059
|
+
/** `outer ∘ inner` — apply `inner` first, then `outer`. */
|
|
6060
|
+
function composeIsometry(outer, inner) {
|
|
6061
|
+
const a = complexAdd(complexMul(outer.a, inner.a), complexMul(outer.b, complexConjugate(inner.b)));
|
|
6062
|
+
const b = complexAdd(complexMul(outer.a, inner.b), complexMul(outer.b, complexConjugate(inner.a)));
|
|
6063
|
+
const det = Math.sqrt(Math.max(EPSILON, a.x * a.x + a.y * a.y - b.x * b.x - b.y * b.y));
|
|
6064
|
+
return {
|
|
6065
|
+
a: complexScale(a, 1 / det),
|
|
6066
|
+
b: complexScale(b, 1 / det)
|
|
6067
|
+
};
|
|
5838
6068
|
}
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
if (!root) return void 0;
|
|
5845
|
-
if (root.item.key === key) return [root];
|
|
5846
|
-
for (const child of root.children ?? []) {
|
|
5847
|
-
const hit = findHyperbolicPath(child, key);
|
|
5848
|
-
if (hit) return [root, ...hit];
|
|
5849
|
-
}
|
|
6069
|
+
function invertIsometry(m) {
|
|
6070
|
+
return {
|
|
6071
|
+
a: complexConjugate(m.a),
|
|
6072
|
+
b: complexScale(m.b, -1)
|
|
6073
|
+
};
|
|
5850
6074
|
}
|
|
5851
|
-
/**
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
6075
|
+
/** Pure translation carrying the origin to `p` (same map as {@link mobiusFromOrigin}). */
|
|
6076
|
+
function translationFromOrigin(p) {
|
|
6077
|
+
const scale = 1 / Math.sqrt(Math.max(EPSILON, 1 - p.x * p.x - p.y * p.y));
|
|
6078
|
+
return {
|
|
6079
|
+
a: {
|
|
6080
|
+
x: scale,
|
|
6081
|
+
y: 0
|
|
6082
|
+
},
|
|
6083
|
+
b: complexScale(p, scale)
|
|
6084
|
+
};
|
|
6085
|
+
}
|
|
6086
|
+
/** `z ↦ e^{iθ}·z`. */
|
|
6087
|
+
function rotationIsometry(theta) {
|
|
6088
|
+
return {
|
|
6089
|
+
a: {
|
|
6090
|
+
x: Math.cos(theta / 2),
|
|
6091
|
+
y: Math.sin(theta / 2)
|
|
6092
|
+
},
|
|
6093
|
+
b: {
|
|
6094
|
+
x: 0,
|
|
6095
|
+
y: 0
|
|
6096
|
+
}
|
|
6097
|
+
};
|
|
5864
6098
|
}
|
|
5865
6099
|
/**
|
|
5866
|
-
*
|
|
5867
|
-
*
|
|
5868
|
-
* `theta` from the true origin. Möbius translation is conformal but not
|
|
5869
|
-
* angle-preserving in the naive sense — a wedge boundary doesn't just shift
|
|
5870
|
-
* by a constant offset when reparented to a new local frame, its exact
|
|
5871
|
-
* transformed angle depends on how far off-center `localOrigin` is. Used to
|
|
5872
|
-
* carry a node's *own* angular wedge (inherited from its parent) into the
|
|
5873
|
-
* local frame of each of its children, rather than approximating it as
|
|
5874
|
-
* "the opposite direction ± half the width" — the same technique used by
|
|
5875
|
-
* reference hyperbolic-tree-browser implementations (e.g. d3-hypertree's
|
|
5876
|
-
* `wedgeTranslate`) to keep the recursive layout accurate at any depth/width.
|
|
6100
|
+
* The view map that sends canonical point `center` to the origin, then
|
|
6101
|
+
* rotates by `rotation` radians: `z ↦ e^{i·rotation}·mobiusToOrigin(z, center)`.
|
|
5877
6102
|
*/
|
|
5878
|
-
function
|
|
5879
|
-
|
|
5880
|
-
x: Math.cos(theta),
|
|
5881
|
-
y: Math.sin(theta)
|
|
5882
|
-
}, localOrigin);
|
|
5883
|
-
return Math.atan2(transformed.y, transformed.x);
|
|
6103
|
+
function viewIsometry(center, rotation = 0) {
|
|
6104
|
+
return composeIsometry(rotationIsometry(rotation), invertIsometry(translationFromOrigin(center)));
|
|
5884
6105
|
}
|
|
5885
|
-
/**
|
|
5886
|
-
function
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
6106
|
+
/** Decompose `m` into the `{ center, rotation }` accepted by {@link viewIsometry}. */
|
|
6107
|
+
function decomposeViewIsometry(m) {
|
|
6108
|
+
return {
|
|
6109
|
+
center: applyIsometry(invertIsometry(m), {
|
|
6110
|
+
x: 0,
|
|
6111
|
+
y: 0
|
|
6112
|
+
}),
|
|
6113
|
+
rotation: 2 * Math.atan2(m.a.y, m.a.x)
|
|
6114
|
+
};
|
|
6115
|
+
}
|
|
6116
|
+
function geodesicSegment(p, q) {
|
|
6117
|
+
const det = p.x * q.y - p.y * q.x;
|
|
6118
|
+
const chord = Math.hypot(q.x - p.x, q.y - p.y);
|
|
6119
|
+
if (Math.abs(det) < 1e-6 * Math.max(chord, EPSILON)) return { kind: `line` };
|
|
6120
|
+
const rp = (p.x * p.x + p.y * p.y + 1) / 2;
|
|
6121
|
+
const rq = (q.x * q.x + q.y * q.y + 1) / 2;
|
|
6122
|
+
const center = {
|
|
6123
|
+
x: (rp * q.y - rq * p.y) / det,
|
|
6124
|
+
y: (p.x * rq - q.x * rp) / det
|
|
6125
|
+
};
|
|
6126
|
+
return {
|
|
6127
|
+
kind: `arc`,
|
|
6128
|
+
center,
|
|
6129
|
+
radius: Math.sqrt(Math.max(0, center.x * center.x + center.y * center.y - 1)),
|
|
6130
|
+
sweep: (p.x - center.x) * (q.y - center.y) - (p.y - center.y) * (q.x - center.x) > 0 ? 1 : 0
|
|
6131
|
+
};
|
|
6132
|
+
}
|
|
6133
|
+
/** SVG path `d` for the geodesic from `p` to `q`, with coordinates multiplied by `scale`. */
|
|
6134
|
+
function geodesicPath(p, q, scale = 1) {
|
|
6135
|
+
const fmt = (n) => (n * scale).toFixed(2);
|
|
6136
|
+
const segment = geodesicSegment(p, q);
|
|
6137
|
+
const start = `M${fmt(p.x)},${fmt(p.y)}`;
|
|
6138
|
+
if (segment.kind === `line` || segment.radius * scale > 1e6) return `${start} L${fmt(q.x)},${fmt(q.y)}`;
|
|
6139
|
+
return `${start} A${fmt(segment.radius)},${fmt(segment.radius)} 0 0 ${segment.sweep} ${fmt(q.x)},${fmt(q.y)}`;
|
|
6140
|
+
}
|
|
6141
|
+
//#endregion
|
|
6142
|
+
//#region src/hyperbolic-tree/view-motion.ts
|
|
6143
|
+
/** Wrap an angle into `(-π, π]`. */
|
|
6144
|
+
function wrapAngle(theta) {
|
|
6145
|
+
return Math.atan2(Math.sin(theta), Math.cos(theta));
|
|
6146
|
+
}
|
|
6147
|
+
function easeInOutCubic(t) {
|
|
6148
|
+
return t < .5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
|
|
6149
|
+
}
|
|
6150
|
+
function prefersReducedMotion$1() {
|
|
6151
|
+
return typeof matchMedia === `function` && matchMedia(`(prefers-reduced-motion: reduce)`).matches;
|
|
5890
6152
|
}
|
|
5891
6153
|
/**
|
|
5892
|
-
*
|
|
5893
|
-
*
|
|
5894
|
-
*
|
|
5895
|
-
*
|
|
5896
|
-
*
|
|
5897
|
-
*
|
|
5898
|
-
* breaks that up — the same purpose served by the small per-node offsets in
|
|
5899
|
-
* reference hyperbolic-tree-browser implementations (e.g. d3-hypertree's
|
|
5900
|
-
* `layoutBergé`, which staggers leaf nodes rather than stacking them exactly
|
|
5901
|
-
* radially).
|
|
6154
|
+
* The view after dragging from `start` to `current` (both disk
|
|
6155
|
+
* coordinates), starting from `baseView`. Applies the hyperbolic translation
|
|
6156
|
+
* along the geodesic carrying the grabbed point to the pointer — a true
|
|
6157
|
+
* isometry, so the grabbed point tracks the pointer exactly and the tree
|
|
6158
|
+
* never distorts. (Adding a Euclidean offset to the canonical center
|
|
6159
|
+
* instead is not an isometry: it lurches badly near the disk boundary.)
|
|
5902
6160
|
*/
|
|
5903
|
-
|
|
6161
|
+
function panView(baseView, start, current) {
|
|
6162
|
+
const toStart = translationFromOrigin(start);
|
|
6163
|
+
return composeIsometry(composeIsometry(toStart, composeIsometry(translationFromOrigin(mobiusToOrigin(current, start)), invertIsometry(toStart))), baseView);
|
|
6164
|
+
}
|
|
5904
6165
|
/**
|
|
5905
|
-
*
|
|
5906
|
-
*
|
|
5907
|
-
*
|
|
5908
|
-
* the
|
|
5909
|
-
*
|
|
5910
|
-
*
|
|
5911
|
-
* its *own* local frame (root starts with the full `[-π, π]`) — across which
|
|
5912
|
-
* its children are placed at hyperbolic distance `curvature` (default `1`).
|
|
5913
|
-
* That wedge is partitioned among the children, weighted by each child's own
|
|
5914
|
-
* subtree size rather than split evenly, and each child's own share is
|
|
5915
|
-
* carried into its local frame via {@link boundaryAngleInLocalFrame} before
|
|
5916
|
-
* recursing. This is a strict partition (slices never overlap and always sum
|
|
5917
|
-
* to the parent's own wedge), which is what guarantees sibling subtrees never
|
|
5918
|
-
* spatially cross regardless of how deep or bushy they are.
|
|
5919
|
-
*
|
|
5920
|
-
* `curvature` scales how far apart (in hyperbolic distance) each generation
|
|
5921
|
-
* sits. Raising it pushes each generation further out, so the Poincaré
|
|
5922
|
-
* metric's own `tanh` compression crowds distant generations toward the
|
|
5923
|
-
* boundary faster — a stronger fisheye, which is why it reads as "zooming
|
|
5924
|
-
* in" on the focus. Lowering it spreads generations out more evenly,
|
|
5925
|
-
* flattening the view toward a Euclidean-feeling layout.
|
|
6166
|
+
* A focus-change animation from view `from` — the old view, re-expressed in
|
|
6167
|
+
* the *new* focus's local frame, so the new focus currently sits somewhere
|
|
6168
|
+
* off-center — to a settled view with the new focus at the center. The
|
|
6169
|
+
* center travels along the hyperbolic geodesic; the rotation is held, so the
|
|
6170
|
+
* tree glides into place without spinning, and the settled view keeps
|
|
6171
|
+
* whatever orientation it arrived with (`settledRotation`).
|
|
5926
6172
|
*/
|
|
5927
|
-
function
|
|
5928
|
-
const
|
|
5929
|
-
|
|
6173
|
+
function focusTransition(from) {
|
|
6174
|
+
const { center, rotation } = decomposeViewIsometry(from);
|
|
6175
|
+
const settledRotation = wrapAngle(rotation);
|
|
5930
6176
|
const origin = {
|
|
5931
6177
|
x: 0,
|
|
5932
6178
|
y: 0
|
|
5933
6179
|
};
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
});
|
|
5941
|
-
const place = (node, position, wedgeStart, wedgeEnd, depth) => {
|
|
5942
|
-
if (depth >= maxDepth) return;
|
|
5943
|
-
const children = node.children ?? [];
|
|
5944
|
-
const n = children.length;
|
|
5945
|
-
if (n === 0) return;
|
|
5946
|
-
const r0 = euclideanRadiusForHyperbolicDistance(curvature);
|
|
5947
|
-
const angularWidth = wedgeEnd - wedgeStart;
|
|
5948
|
-
const weights = children.map((child) => subtreeSize(child, maxDepth - depth - 1));
|
|
5949
|
-
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
5950
|
-
let cursor = wedgeStart;
|
|
5951
|
-
children.forEach((child, index) => {
|
|
5952
|
-
const share = n === 1 ? angularWidth : angularWidth * weights[index] / totalWeight;
|
|
5953
|
-
const childStart = cursor;
|
|
5954
|
-
const childEnd = cursor + share;
|
|
5955
|
-
cursor = childEnd;
|
|
5956
|
-
const theta = (childStart + childEnd) / 2 + (n === 1 ? stableUnitJitter(child.item.key) * Math.min(CHAIN_JITTER_RADIANS, share * .4) : 0);
|
|
5957
|
-
const local = {
|
|
5958
|
-
x: r0 * Math.cos(theta),
|
|
5959
|
-
y: r0 * Math.sin(theta)
|
|
5960
|
-
};
|
|
5961
|
-
const canonicalPosition = mobiusFromOrigin(local, position);
|
|
5962
|
-
map.set(child.item.key, {
|
|
5963
|
-
node: child,
|
|
5964
|
-
depth: depth + 1,
|
|
5965
|
-
parentKey: node.item.key,
|
|
5966
|
-
position: canonicalPosition,
|
|
5967
|
-
angularShare: share
|
|
5968
|
-
});
|
|
5969
|
-
const localStart = boundaryAngleInLocalFrame(childStart, local);
|
|
5970
|
-
let localWidth = boundaryAngleInLocalFrame(childEnd, local) - localStart;
|
|
5971
|
-
if (localWidth <= 0) localWidth += 2 * Math.PI;
|
|
5972
|
-
place(child, canonicalPosition, localStart, localStart + localWidth, depth + 1);
|
|
5973
|
-
});
|
|
6180
|
+
return {
|
|
6181
|
+
settledRotation,
|
|
6182
|
+
viewAt: (t) => {
|
|
6183
|
+
const eased = easeInOutCubic(Math.min(1, Math.max(0, t)));
|
|
6184
|
+
return viewIsometry(geodesicInterpolate(center, origin, eased), settledRotation);
|
|
6185
|
+
}
|
|
5974
6186
|
};
|
|
5975
|
-
place(root, origin, -Math.PI, Math.PI, 0);
|
|
5976
|
-
return map;
|
|
5977
6187
|
}
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
for (const neighbour of neighbours) if (!ring.has(neighbour) && canonical.has(neighbour)) {
|
|
5993
|
-
ring.set(neighbour, distance);
|
|
5994
|
-
next.push(neighbour);
|
|
5995
|
-
}
|
|
6188
|
+
//#endregion
|
|
6189
|
+
//#region src/hyperbolic-tree/gestures.ts
|
|
6190
|
+
const MIN_ZOOM = .5;
|
|
6191
|
+
/**
|
|
6192
|
+
* Keep the view center over the disk (so you can't zoom off into empty
|
|
6193
|
+
* space), and re-center entirely once the whole disk fits again.
|
|
6194
|
+
*/
|
|
6195
|
+
function clampZoomState(state) {
|
|
6196
|
+
const zoom = Math.min(8, Math.max(MIN_ZOOM, state.zoom));
|
|
6197
|
+
if (zoom <= 1) return {
|
|
6198
|
+
zoom,
|
|
6199
|
+
offset: {
|
|
6200
|
+
x: 0,
|
|
6201
|
+
y: 0
|
|
5996
6202
|
}
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6203
|
+
};
|
|
6204
|
+
const limit = zoom - 1;
|
|
6205
|
+
const length = Math.hypot(state.offset.x, state.offset.y);
|
|
6206
|
+
return {
|
|
6207
|
+
zoom,
|
|
6208
|
+
offset: length > limit ? {
|
|
6209
|
+
x: state.offset.x * limit / length,
|
|
6210
|
+
y: state.offset.y * limit / length
|
|
6211
|
+
} : state.offset
|
|
6212
|
+
};
|
|
6000
6213
|
}
|
|
6001
6214
|
/**
|
|
6002
|
-
*
|
|
6003
|
-
*
|
|
6004
|
-
* node, projected onto the Poincaré disk with the focus at its center.
|
|
6005
|
-
*
|
|
6006
|
-
* @param root True tree root. `undefined` yields an empty layout.
|
|
6007
|
-
* @param options `focusedKey` (default the true root; unknown keys fall back
|
|
6008
|
-
* to the true root), `maxRadius` (default unlimited), `maxDepth` (safety
|
|
6009
|
-
* cap on canonical-position recursion depth, default 24), `centerOverride`
|
|
6010
|
-
* (project around this canonical-space point instead of the focus node's
|
|
6011
|
-
* own position — used to animate smoothly between foci).
|
|
6215
|
+
* Zoom by `factor` keeping the disk point under `screenPoint` fixed on
|
|
6216
|
+
* screen — the "zoom where I'm pointing" behaviour of maps and image viewers.
|
|
6012
6217
|
*/
|
|
6013
|
-
function
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6218
|
+
function zoomAt(state, screenPoint, factor) {
|
|
6219
|
+
const zoom = Math.min(8, Math.max(MIN_ZOOM, state.zoom * factor));
|
|
6220
|
+
const disk = screenToDisk(screenPoint, state);
|
|
6221
|
+
return clampZoomState({
|
|
6222
|
+
zoom,
|
|
6223
|
+
offset: {
|
|
6224
|
+
x: screenPoint.x - zoom * disk.x,
|
|
6225
|
+
y: screenPoint.y - zoom * disk.y
|
|
6226
|
+
}
|
|
6227
|
+
});
|
|
6228
|
+
}
|
|
6229
|
+
/**
|
|
6230
|
+
* A client-space position in screen units: disk radii from the SVG's center,
|
|
6231
|
+
* before zoom. The disk fills the SVG's smaller dimension, with the
|
|
6232
|
+
* viewBox's 5% margin.
|
|
6233
|
+
*/
|
|
6234
|
+
function clientToScreen(rect, clientX, clientY) {
|
|
6235
|
+
const unitsPerPixel = 2.1 / (Math.min(rect.width, rect.height) || 1);
|
|
6236
|
+
return {
|
|
6237
|
+
x: (clientX - (rect.left + rect.width / 2)) * unitsPerPixel,
|
|
6238
|
+
y: (clientY - (rect.top + rect.height / 2)) * unitsPerPixel
|
|
6017
6239
|
};
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
const
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
edges: []
|
|
6240
|
+
}
|
|
6241
|
+
function screenToDisk(p, state) {
|
|
6242
|
+
const zoom = state.zoom || 1;
|
|
6243
|
+
return {
|
|
6244
|
+
x: (p.x - state.offset.x) / zoom,
|
|
6245
|
+
y: (p.y - state.offset.y) / zoom
|
|
6025
6246
|
};
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6247
|
+
}
|
|
6248
|
+
/** Is `event` on empty disk space (the SVG itself or the boundary circle), rather than a node? */
|
|
6249
|
+
function isBackground(event) {
|
|
6250
|
+
const target = event.target;
|
|
6251
|
+
return target === event.currentTarget || target?.classList.contains(`disk-boundary`) === true;
|
|
6252
|
+
}
|
|
6253
|
+
function createGestureHandlers(host) {
|
|
6254
|
+
const pointers = /* @__PURE__ */ new Map();
|
|
6255
|
+
let pan;
|
|
6256
|
+
let pinch;
|
|
6257
|
+
const screenOf = (event) => {
|
|
6258
|
+
const rect = host.svgRect();
|
|
6259
|
+
return rect ? clientToScreen(rect, event.clientX, event.clientY) : void 0;
|
|
6260
|
+
};
|
|
6261
|
+
const diskOf = (screen) => clampToDisk(screenToDisk(screen, host.zoomState()), .98);
|
|
6262
|
+
const midpoint = (a, b) => ({
|
|
6263
|
+
x: (a.x + b.x) / 2,
|
|
6264
|
+
y: (a.y + b.y) / 2
|
|
6265
|
+
});
|
|
6266
|
+
const startPan = (pointerId, screen) => {
|
|
6267
|
+
pan = {
|
|
6268
|
+
pointerId,
|
|
6269
|
+
start: diskOf(screen),
|
|
6270
|
+
baseView: host.takeView()
|
|
6271
|
+
};
|
|
6272
|
+
};
|
|
6273
|
+
const startPinch = () => {
|
|
6274
|
+
const [[idA, a], [idB, b]] = [...pointers.entries()];
|
|
6275
|
+
pan = void 0;
|
|
6276
|
+
const { zoom } = host.zoomState();
|
|
6277
|
+
pinch = {
|
|
6278
|
+
ids: [idA, idB],
|
|
6279
|
+
startDistance: Math.max(1e-6, Math.hypot(a.x - b.x, a.y - b.y)),
|
|
6280
|
+
startZoom: zoom,
|
|
6281
|
+
anchor: screenToDisk(midpoint(a, b), host.zoomState())
|
|
6282
|
+
};
|
|
6283
|
+
};
|
|
6284
|
+
return {
|
|
6285
|
+
onPointerDown(event) {
|
|
6286
|
+
if (!isBackground(event)) return;
|
|
6287
|
+
host.focusTree();
|
|
6288
|
+
const screen = screenOf(event);
|
|
6289
|
+
if (!screen) return;
|
|
6290
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
6291
|
+
pointers.set(event.pointerId, screen);
|
|
6292
|
+
if (pointers.size === 2 && host.zoomGesturesEnabled()) startPinch();
|
|
6293
|
+
else if (pointers.size === 1) startPan(event.pointerId, screen);
|
|
6294
|
+
},
|
|
6295
|
+
onPointerMove(event) {
|
|
6296
|
+
if (!pointers.has(event.pointerId)) return;
|
|
6297
|
+
const screen = screenOf(event);
|
|
6298
|
+
if (!screen) return;
|
|
6299
|
+
pointers.set(event.pointerId, screen);
|
|
6300
|
+
if (pinch) {
|
|
6301
|
+
const a = pointers.get(pinch.ids[0]);
|
|
6302
|
+
const b = pointers.get(pinch.ids[1]);
|
|
6303
|
+
if (!a || !b) return;
|
|
6304
|
+
const zoom = pinch.startZoom * Math.hypot(a.x - b.x, a.y - b.y) / pinch.startDistance;
|
|
6305
|
+
const mid = midpoint(a, b);
|
|
6306
|
+
const clampedZoom = Math.min(8, Math.max(MIN_ZOOM, zoom));
|
|
6307
|
+
host.setZoomState(clampZoomState({
|
|
6308
|
+
zoom: clampedZoom,
|
|
6309
|
+
offset: {
|
|
6310
|
+
x: mid.x - clampedZoom * pinch.anchor.x,
|
|
6311
|
+
y: mid.y - clampedZoom * pinch.anchor.y
|
|
6312
|
+
}
|
|
6313
|
+
}));
|
|
6314
|
+
} else if (pan && pan.pointerId === event.pointerId) host.setView(panView(pan.baseView, pan.start, diskOf(screen)));
|
|
6315
|
+
},
|
|
6316
|
+
onPointerUp(event) {
|
|
6317
|
+
if (!pointers.delete(event.pointerId)) return;
|
|
6318
|
+
if (pinch) {
|
|
6319
|
+
pinch = void 0;
|
|
6320
|
+
const [remaining] = [...pointers.entries()];
|
|
6321
|
+
if (remaining) startPan(remaining[0], remaining[1]);
|
|
6322
|
+
} else if (pan?.pointerId === event.pointerId) pan = void 0;
|
|
6323
|
+
},
|
|
6324
|
+
/**
|
|
6325
|
+
* Ctrl/⌘-wheel — which is also what a trackpad pinch produces — always
|
|
6326
|
+
* zooms while the pointer is over the tree. A plain wheel only zooms
|
|
6327
|
+
* once the tree has keyboard focus (the library's focus-gated wheel
|
|
6328
|
+
* convention), so scrolling past it doesn't get hijacked.
|
|
6329
|
+
*/
|
|
6330
|
+
onWheel(event) {
|
|
6331
|
+
const modified = event.ctrlKey || event.metaKey;
|
|
6332
|
+
if (!host.zoomGesturesEnabled() || !modified && !host.hasFocus()) return;
|
|
6333
|
+
const screen = screenOf(event);
|
|
6334
|
+
if (!screen) return;
|
|
6335
|
+
event.preventDefault();
|
|
6336
|
+
const delta = event.deltaMode === 1 ? event.deltaY * 16 : event.deltaY;
|
|
6337
|
+
const factor = Math.exp(-delta * (modified ? .01 : .002));
|
|
6338
|
+
host.setZoomState(zoomAt(host.zoomState(), screen, factor));
|
|
6339
|
+
},
|
|
6340
|
+
/** Forget any in-progress pan (e.g. when a focus change takes over the view). */
|
|
6341
|
+
cancel() {
|
|
6342
|
+
pan = void 0;
|
|
6343
|
+
pinch = void 0;
|
|
6344
|
+
pointers.clear();
|
|
6345
|
+
}
|
|
6346
|
+
};
|
|
6347
|
+
}
|
|
6348
|
+
//#endregion
|
|
6349
|
+
//#region src/hyperbolic-tree/hyperbolictree-styles.ts
|
|
6350
|
+
/** Shadow-DOM styles for `ixfx-hyperbolictree-list`. */
|
|
6351
|
+
const hyperbolicTreeStyles = css`
|
|
6352
|
+
:host {
|
|
6353
|
+
--hyperbolictree-node-color: var(--surface-4);
|
|
6354
|
+
--hyperbolictree-node-text: var(--surface-3-text);
|
|
6355
|
+
--hyperbolictree-edge-color: var(--surface-6);
|
|
6356
|
+
--hyperbolictree-boundary-color: var(--border-subtle);
|
|
6357
|
+
/* Real screen px — converted to SVG units from the element's rendered size. */
|
|
6358
|
+
--hyperbolictree-font-size: 13px;
|
|
6359
|
+
--hyperbolictree-box-color: var(--hyperbolictree-node-color);
|
|
6360
|
+
--hyperbolictree-box-text: var(--surface-4-text);
|
|
6361
|
+
--hyperbolictree-box-border: var(--surface-2);
|
|
6362
|
+
--hyperbolictree-branch-hue-start: 250;
|
|
6363
|
+
--hyperbolictree-branch-chroma: 0.13;
|
|
6364
|
+
--hyperbolictree-branch-lightness: 62%;
|
|
6365
|
+
--hyperbolictree-branch-mix: 55%;
|
|
6366
|
+
|
|
6367
|
+
display: block;
|
|
6368
|
+
position: relative;
|
|
6369
|
+
overflow: hidden;
|
|
6370
|
+
outline: none;
|
|
6371
|
+
overscroll-behavior: contain;
|
|
6372
|
+
}
|
|
6373
|
+
|
|
6374
|
+
/*
|
|
6375
|
+
* delegatesFocus means the host itself reports as focused whenever
|
|
6376
|
+
* the shadow DOM's focused descendant (the svg) is — without this,
|
|
6377
|
+
* the host also picks up the browser's own default focus ring/fill
|
|
6378
|
+
* sized to the whole box.
|
|
6379
|
+
*/
|
|
6380
|
+
:host(:focus),
|
|
6381
|
+
:host(:focus-visible) {
|
|
6382
|
+
outline: none;
|
|
6383
|
+
}
|
|
6384
|
+
|
|
6385
|
+
.hyperbolictree {
|
|
6386
|
+
display: block;
|
|
6387
|
+
width: 100%;
|
|
6388
|
+
height: 100%;
|
|
6389
|
+
outline: none;
|
|
6390
|
+
touch-action: none;
|
|
6391
|
+
overscroll-behavior: contain;
|
|
6392
|
+
}
|
|
6393
|
+
|
|
6394
|
+
.disk-boundary {
|
|
6395
|
+
fill: none;
|
|
6396
|
+
stroke: var(--hyperbolictree-boundary-color);
|
|
6397
|
+
stroke-width: 1;
|
|
6398
|
+
}
|
|
6399
|
+
|
|
6400
|
+
.edge {
|
|
6401
|
+
fill: none;
|
|
6402
|
+
stroke: var(--hyperbolictree-edge-color);
|
|
6403
|
+
/* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
|
|
6404
|
+
stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
|
|
6405
|
+
/* Constant on-screen width, however far the disk is zoomed. */
|
|
6406
|
+
vector-effect: non-scaling-stroke;
|
|
6407
|
+
}
|
|
6408
|
+
|
|
6409
|
+
.node {
|
|
6410
|
+
cursor: pointer;
|
|
6411
|
+
}
|
|
6412
|
+
|
|
6413
|
+
/*
|
|
6414
|
+
* color-by-branch: each top-level branch gets an evenly-spaced hue,
|
|
6415
|
+
* mixed into the normal node/box/edge colour (rather than replacing it)
|
|
6416
|
+
* so it stays in keeping with — and legible in — the current theme.
|
|
6417
|
+
* Only sets the base colour vars, so selected/tickled states still win.
|
|
6418
|
+
*/
|
|
6419
|
+
:host([color-by-branch]) .node,
|
|
6420
|
+
:host([color-by-branch]) .edge {
|
|
6421
|
+
--_branch-color: oklch(
|
|
6422
|
+
var(--hyperbolictree-branch-lightness)
|
|
6423
|
+
var(--hyperbolictree-branch-chroma)
|
|
6424
|
+
calc(var(--hyperbolictree-branch-hue-start) + var(--branch-index) * 360 / var(--branch-count))
|
|
6425
|
+
);
|
|
6426
|
+
}
|
|
6427
|
+
|
|
6428
|
+
:host([color-by-branch]) .node {
|
|
6429
|
+
--_node-base: color-mix(in oklab, var(--hyperbolictree-node-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
|
|
6430
|
+
--_box-base: color-mix(in oklab, var(--hyperbolictree-box-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
|
|
6431
|
+
}
|
|
6432
|
+
|
|
6433
|
+
:host([color-by-branch]) .edge {
|
|
6434
|
+
stroke: color-mix(in oklab, var(--hyperbolictree-edge-color), var(--_branch-color) 50%);
|
|
6435
|
+
}
|
|
6436
|
+
|
|
6437
|
+
.node-hit {
|
|
6438
|
+
fill: transparent;
|
|
6439
|
+
pointer-events: all;
|
|
6440
|
+
}
|
|
6441
|
+
|
|
6442
|
+
.node-dot {
|
|
6443
|
+
/* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
|
|
6444
|
+
fill: color-mix(in oklab, var(--_node-base, var(--hyperbolictree-node-color)), var(--accent) calc(var(--node-weight, 0.5) * 50%));
|
|
6445
|
+
stroke: var(--surface-2);
|
|
6446
|
+
stroke-width: 1;
|
|
6447
|
+
pointer-events: none;
|
|
6448
|
+
}
|
|
6449
|
+
|
|
6450
|
+
.node-box {
|
|
6451
|
+
fill: color-mix(in oklab, var(--_box-base, var(--hyperbolictree-box-color)), var(--accent) calc(var(--node-weight, 0.5) * 25%));
|
|
6452
|
+
stroke: var(--hyperbolictree-box-border);
|
|
6453
|
+
stroke-width: 1.5;
|
|
6454
|
+
/* Keep the border crisp however far the box has been scaled down. */
|
|
6455
|
+
vector-effect: non-scaling-stroke;
|
|
6456
|
+
}
|
|
6457
|
+
|
|
6458
|
+
/* Text-less stand-in for a box too small, or too crowded, to label. */
|
|
6459
|
+
.node-box.marker {
|
|
6460
|
+
stroke-width: 1;
|
|
6461
|
+
}
|
|
6462
|
+
|
|
6463
|
+
/* Pure user-space font-size; set per render from --hyperbolictree-font-size. */
|
|
6464
|
+
.hyperbolictree-label {
|
|
6465
|
+
fill: var(--hyperbolictree-node-text);
|
|
6466
|
+
cursor: pointer;
|
|
6467
|
+
user-select: none;
|
|
6468
|
+
}
|
|
6469
|
+
|
|
6470
|
+
.box-label {
|
|
6471
|
+
fill: var(--hyperbolictree-box-text);
|
|
6472
|
+
pointer-events: none;
|
|
6473
|
+
}
|
|
6474
|
+
|
|
6475
|
+
/*
|
|
6476
|
+
* State fills come paired with their theme text tokens: a box label sits
|
|
6477
|
+
* on the state fill itself, so e.g. --item-bg-selected needs
|
|
6478
|
+
* --item-text-selected on top, not the resting box text colour.
|
|
6479
|
+
*/
|
|
6480
|
+
.node.tickled .node-dot,
|
|
6481
|
+
.node.tickled .node-box {
|
|
6482
|
+
fill: var(--item-bg-tickled);
|
|
6483
|
+
}
|
|
6484
|
+
|
|
6485
|
+
.node.tickled .box-label {
|
|
6486
|
+
fill: var(--item-text-tickled);
|
|
6487
|
+
}
|
|
6488
|
+
|
|
6489
|
+
.node.selected .node-dot,
|
|
6490
|
+
.node.selected .node-box {
|
|
6491
|
+
fill: var(--item-bg-selected);
|
|
6492
|
+
}
|
|
6493
|
+
|
|
6494
|
+
.node.selected .box-label {
|
|
6495
|
+
fill: var(--item-text-selected);
|
|
6496
|
+
}
|
|
6497
|
+
|
|
6498
|
+
.node.selected.tickled .node-dot,
|
|
6499
|
+
.node.selected.tickled .node-box {
|
|
6500
|
+
fill: var(--item-bg-selected-tickled);
|
|
6501
|
+
}
|
|
6502
|
+
|
|
6503
|
+
.node.focused .node-dot,
|
|
6504
|
+
.node.focused .node-box {
|
|
6505
|
+
stroke: var(--accent);
|
|
6506
|
+
stroke-width: 3;
|
|
6507
|
+
}
|
|
6508
|
+
|
|
6509
|
+
/* A hovered label may sit over busy neighbours — give it a halo so it always reads. */
|
|
6510
|
+
.node.tickled .hyperbolictree-label:not(.box-label) {
|
|
6511
|
+
paint-order: stroke;
|
|
6512
|
+
stroke: var(--surface-2);
|
|
6513
|
+
stroke-width: 4px;
|
|
6514
|
+
stroke-linejoin: round;
|
|
6515
|
+
}
|
|
6516
|
+
|
|
6517
|
+
.node.filtered {
|
|
6518
|
+
opacity: 0.3;
|
|
6519
|
+
}
|
|
6520
|
+
|
|
6521
|
+
.node.loading .node-dot,
|
|
6522
|
+
.node.loading .node-box {
|
|
6523
|
+
opacity: 0.6;
|
|
6524
|
+
}
|
|
6525
|
+
|
|
6526
|
+
|
|
6527
|
+
/* Child preview: a clickable list of a crowded node's children. */
|
|
6528
|
+
.child-preview {
|
|
6529
|
+
position: absolute;
|
|
6530
|
+
z-index: 1;
|
|
6531
|
+
min-width: 8rem;
|
|
6532
|
+
max-width: min(18rem, 60%);
|
|
6533
|
+
max-height: 45%;
|
|
6534
|
+
overflow: auto;
|
|
6535
|
+
padding: var(--space-xs, 4px);
|
|
6536
|
+
background: var(--surface-3);
|
|
6537
|
+
color: var(--surface-3-text);
|
|
6538
|
+
border: var(--border-subtle);
|
|
6539
|
+
border-radius: var(--radius-m, 6px);
|
|
6540
|
+
box-shadow: var(--shadow-m);
|
|
6541
|
+
font-size: var(--hyperbolictree-font-size);
|
|
6542
|
+
transform: translate(-50%, 14px);
|
|
6543
|
+
}
|
|
6544
|
+
|
|
6545
|
+
.child-preview.above {
|
|
6546
|
+
transform: translate(-50%, calc(-100% - 14px));
|
|
6547
|
+
}
|
|
6548
|
+
|
|
6549
|
+
.child-preview-title {
|
|
6550
|
+
padding: 2px 6px 4px;
|
|
6551
|
+
font-weight: 600;
|
|
6552
|
+
overflow: hidden;
|
|
6553
|
+
text-overflow: ellipsis;
|
|
6554
|
+
white-space: nowrap;
|
|
6555
|
+
}
|
|
6556
|
+
|
|
6557
|
+
.child-preview ul {
|
|
6558
|
+
margin: 0;
|
|
6559
|
+
padding: 0;
|
|
6560
|
+
list-style: none;
|
|
6561
|
+
}
|
|
6562
|
+
|
|
6563
|
+
.child-preview button {
|
|
6564
|
+
display: block;
|
|
6565
|
+
width: 100%;
|
|
6566
|
+
padding: 2px 6px;
|
|
6567
|
+
border: none;
|
|
6568
|
+
border-radius: var(--radius-s, 4px);
|
|
6569
|
+
background: none;
|
|
6570
|
+
color: inherit;
|
|
6571
|
+
font: inherit;
|
|
6572
|
+
text-align: start;
|
|
6573
|
+
overflow: hidden;
|
|
6574
|
+
text-overflow: ellipsis;
|
|
6575
|
+
white-space: nowrap;
|
|
6576
|
+
cursor: pointer;
|
|
6577
|
+
}
|
|
6578
|
+
|
|
6579
|
+
.child-preview button:hover {
|
|
6580
|
+
background: var(--item-bg-tickled);
|
|
6581
|
+
color: var(--item-text-tickled);
|
|
6582
|
+
}
|
|
6583
|
+
|
|
6584
|
+
.child-preview-more {
|
|
6585
|
+
padding: 2px 6px;
|
|
6586
|
+
color: var(--surface-muted-text);
|
|
6587
|
+
}
|
|
6588
|
+
`;
|
|
6589
|
+
//#endregion
|
|
6590
|
+
//#region src/hyperbolic-tree/layout.ts
|
|
6591
|
+
const ID_SEPARATOR = `\u001F`;
|
|
6592
|
+
/**
|
|
6593
|
+
* Path-derived ids for `children` of the node with id `parentId`. `TreeItem.key`
|
|
6594
|
+
* is meant to be unique, but real data (e.g. a file system, where `readme.md`
|
|
6595
|
+
* recurs in many folders) often only has keys unique among siblings. Keying
|
|
6596
|
+
* the layout on `item.key` alone would silently merge those nodes — edges get
|
|
6597
|
+
* attached to the wrong parent and clicks focus the wrong node — so the
|
|
6598
|
+
* layout identifies nodes by their key path instead. Repeated keys among
|
|
6599
|
+
* siblings are disambiguated by occurrence count. The root's id is its key.
|
|
6600
|
+
*/
|
|
6601
|
+
function hyperbolicChildIds(parentId, children) {
|
|
6602
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6603
|
+
return children.map((child) => {
|
|
6604
|
+
const key = child.item.key;
|
|
6605
|
+
const count = seen.get(key) ?? 0;
|
|
6606
|
+
seen.set(key, count + 1);
|
|
6607
|
+
return `${parentId}${ID_SEPARATOR}${count === 0 ? key : `${key}#${count}`}`;
|
|
6608
|
+
});
|
|
6609
|
+
}
|
|
6610
|
+
/** Depth (root is 0) of the node with layout id `id`. */
|
|
6611
|
+
function hyperbolicIdDepth(id) {
|
|
6612
|
+
return id.split(ID_SEPARATOR).length - 1;
|
|
6613
|
+
}
|
|
6614
|
+
/**
|
|
6615
|
+
* Layout id of the root's child that `id` descends from (or is) — i.e. which
|
|
6616
|
+
* top-level branch it belongs to. `undefined` for the root itself.
|
|
6617
|
+
*/
|
|
6618
|
+
function hyperbolicBranchId(id) {
|
|
6619
|
+
const parts = id.split(ID_SEPARATOR);
|
|
6620
|
+
return parts.length < 2 ? void 0 : `${parts[0]}${ID_SEPARATOR}${parts[1]}`;
|
|
6621
|
+
}
|
|
6622
|
+
/** Whether layout id `id` is a strict descendant of `ancestorId`. */
|
|
6623
|
+
function isHyperbolicDescendantId(id, ancestorId) {
|
|
6624
|
+
return id.startsWith(`${ancestorId}${ID_SEPARATOR}`);
|
|
6625
|
+
}
|
|
6626
|
+
/** Chain of nodes from `root` down to the node with layout id `id`, or `undefined` if absent. */
|
|
6627
|
+
function findHyperbolicPathById(root, id) {
|
|
6628
|
+
if (!root) return void 0;
|
|
6629
|
+
const walk = (node, nodeId) => {
|
|
6630
|
+
if (nodeId === id) return [node];
|
|
6631
|
+
if (!isHyperbolicDescendantId(id, nodeId)) return void 0;
|
|
6632
|
+
const children = node.children ?? [];
|
|
6633
|
+
const ids = hyperbolicChildIds(nodeId, children);
|
|
6634
|
+
for (let i = 0; i < children.length; i++) {
|
|
6635
|
+
const hit = walk(children[i], ids[i]);
|
|
6636
|
+
if (hit) return [node, ...hit];
|
|
6637
|
+
}
|
|
6638
|
+
};
|
|
6639
|
+
return walk(root, root.item.key);
|
|
6640
|
+
}
|
|
6641
|
+
/** Layout id of `target` (matched by object identity) within `root`, or `undefined` if absent. */
|
|
6642
|
+
function findHyperbolicIdOf(root, target) {
|
|
6643
|
+
if (!root) return void 0;
|
|
6644
|
+
const walk = (node, nodeId) => {
|
|
6645
|
+
if (node === target) return nodeId;
|
|
6646
|
+
const children = node.children ?? [];
|
|
6647
|
+
const ids = hyperbolicChildIds(nodeId, children);
|
|
6648
|
+
for (let i = 0; i < children.length; i++) {
|
|
6649
|
+
const hit = walk(children[i], ids[i]);
|
|
6650
|
+
if (hit !== void 0) return hit;
|
|
6651
|
+
}
|
|
6652
|
+
};
|
|
6653
|
+
return walk(root, root.item.key);
|
|
6654
|
+
}
|
|
6655
|
+
/** Key path (from the root's children) to the first node, depth-first, satisfying `predicate`. */
|
|
6656
|
+
function findHyperbolicKeyPath(nodes, predicate, prefix = []) {
|
|
6657
|
+
for (const node of nodes) {
|
|
6658
|
+
const path = [...prefix, node.item.key];
|
|
6659
|
+
if (predicate(node)) return path;
|
|
6660
|
+
const hit = node.children?.length ? findHyperbolicKeyPath(node.children, predicate, path) : void 0;
|
|
6661
|
+
if (hit) return hit;
|
|
6662
|
+
}
|
|
6663
|
+
}
|
|
6664
|
+
/** Depth-first search for the node with `key`. Returns `undefined` when absent. */
|
|
6665
|
+
function findHyperbolicNode(root, key) {
|
|
6666
|
+
if (!root) return void 0;
|
|
6667
|
+
if (root.item.key === key) return root;
|
|
6668
|
+
for (const child of root.children ?? []) {
|
|
6669
|
+
const hit = findHyperbolicNode(child, key);
|
|
6670
|
+
if (hit) return hit;
|
|
6671
|
+
}
|
|
6672
|
+
}
|
|
6673
|
+
/**
|
|
6674
|
+
* Chain of nodes from `root` down to (and including) the node with `key`.
|
|
6675
|
+
* Returns `undefined` when the key is absent. The first entry is always `root`.
|
|
6676
|
+
*/
|
|
6677
|
+
function findHyperbolicPath(root, key) {
|
|
6678
|
+
if (!root) return void 0;
|
|
6679
|
+
if (root.item.key === key) return [root];
|
|
6680
|
+
for (const child of root.children ?? []) {
|
|
6681
|
+
const hit = findHyperbolicPath(child, key);
|
|
6682
|
+
if (hit) return [root, ...hit];
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
/**
|
|
6686
|
+
* Number of nodes in `node`'s subtree, counting `node` itself, up to
|
|
6687
|
+
* `remainingDepth` further levels (matching the canonical-position recursion's
|
|
6688
|
+
* own `maxDepth` cutoff, so weighting stays consistent with what's actually
|
|
6689
|
+
* laid out).
|
|
6690
|
+
*/
|
|
6691
|
+
function subtreeSize(node, remainingDepth) {
|
|
6692
|
+
if (remainingDepth <= 0) return 1;
|
|
6693
|
+
const children = node.children ?? [];
|
|
6694
|
+
if (children.length === 0) return 1;
|
|
6695
|
+
let total = 1;
|
|
6696
|
+
for (const child of children) total += subtreeSize(child, remainingDepth - 1);
|
|
6697
|
+
return total;
|
|
6698
|
+
}
|
|
6699
|
+
/**
|
|
6700
|
+
* Angle (as seen from `localOrigin`, i.e. after translating `localOrigin` to
|
|
6701
|
+
* the disk's center) of the disk-boundary point that currently sits at angle
|
|
6702
|
+
* `theta` from the true origin. Möbius translation is conformal but not
|
|
6703
|
+
* angle-preserving in the naive sense — a wedge boundary doesn't just shift
|
|
6704
|
+
* by a constant offset when reparented to a new local frame, its exact
|
|
6705
|
+
* transformed angle depends on how far off-center `localOrigin` is. Used to
|
|
6706
|
+
* carry a node's *own* angular wedge (inherited from its parent) into the
|
|
6707
|
+
* local frame of each of its children, rather than approximating it as
|
|
6708
|
+
* "the opposite direction ± half the width" — the same technique used by
|
|
6709
|
+
* reference hyperbolic-tree-browser implementations (e.g. d3-hypertree's
|
|
6710
|
+
* `wedgeTranslate`) to keep the recursive layout accurate at any depth/width.
|
|
6711
|
+
*/
|
|
6712
|
+
function boundaryAngleInLocalFrame(theta, localOrigin) {
|
|
6713
|
+
const transformed = mobiusToOrigin({
|
|
6714
|
+
x: Math.cos(theta),
|
|
6715
|
+
y: Math.sin(theta)
|
|
6716
|
+
}, localOrigin);
|
|
6717
|
+
return Math.atan2(transformed.y, transformed.x);
|
|
6718
|
+
}
|
|
6719
|
+
/** Deterministic pseudo-random value in `[-1, 1]` derived from `key`, stable across renders. */
|
|
6720
|
+
function stableUnitJitter(key) {
|
|
6721
|
+
let hash = 0;
|
|
6722
|
+
for (let i = 0; i < key.length; i++) hash = Math.imul(hash, 31) + key.charCodeAt(i) | 0;
|
|
6723
|
+
return (hash >>> 0) % 1e3 / 1e3 * 2 - 1;
|
|
6724
|
+
}
|
|
6725
|
+
/**
|
|
6726
|
+
* A single unbranching child (no siblings to fan against) would otherwise sit
|
|
6727
|
+
* at the exact bisector of its inherited wedge — i.e. dead-center, continuing
|
|
6728
|
+
* in a perfectly straight line from its own parent's edge. For a chain of
|
|
6729
|
+
* such nodes (a deeply-nested path with no branching), that reads as one
|
|
6730
|
+
* unbroken line cutting across the disk rather than a legible sequence of
|
|
6731
|
+
* edges. Nudging by a small, key-derived (so stable across re-renders) angle
|
|
6732
|
+
* breaks that up — the same purpose served by the small per-node offsets in
|
|
6733
|
+
* reference hyperbolic-tree-browser implementations (e.g. d3-hypertree's
|
|
6734
|
+
* `layoutBergé`, which staggers leaf nodes rather than stacking them exactly
|
|
6735
|
+
* radially).
|
|
6736
|
+
*/
|
|
6737
|
+
const CHAIN_JITTER_RADIANS = .22;
|
|
6738
|
+
/** Default minimum hyperbolic distance between adjacent siblings. See {@link childDistance}. */
|
|
6739
|
+
const DEFAULT_SIBLING_SPACING = .5;
|
|
6740
|
+
/**
|
|
6741
|
+
* Hyperbolic distance from parent to a child whose angular slice (in the
|
|
6742
|
+
* parent's frame) is `share`, such that the child sits at least `spacing`
|
|
6743
|
+
* from a neighbour one slice-width away: from the hyperbolic law of cosines
|
|
6744
|
+
* for an isosceles triangle, `sinh(d/2) = sinh(L)·sin(Δθ/2)`, solved for L.
|
|
6745
|
+
* Never less than `minimum` (the `curvature`), and capped so a sliver-thin
|
|
6746
|
+
* slice can't fling a child off to infinity.
|
|
6747
|
+
*
|
|
6748
|
+
* This is what makes panning *useful* in a crowded fan: panning is an
|
|
6749
|
+
* isometry — it can't change the hyperbolic distance between siblings, only
|
|
6750
|
+
* move them to where the disk magnifies more. If siblings are laid out
|
|
6751
|
+
* nearly on top of each other in hyperbolic terms, no amount of dragging
|
|
6752
|
+
* separates them; guaranteeing a minimum spacing means dragging a crowd
|
|
6753
|
+
* towards the center always fans it out.
|
|
6754
|
+
*/
|
|
6755
|
+
function childDistance(share, spacing, minimum) {
|
|
6756
|
+
if (spacing <= 0) return minimum;
|
|
6757
|
+
const halfAngle = Math.min(share, Math.PI) / 2;
|
|
6758
|
+
const needed = Math.asinh(Math.sinh(spacing / 2) / Math.max(Math.sin(halfAngle), 1e-6));
|
|
6759
|
+
return Math.min(Math.max(minimum, needed), minimum + 6);
|
|
6760
|
+
}
|
|
6761
|
+
/**
|
|
6762
|
+
* Canonical (true-root-anchored) hyperbolic position of every currently-loaded
|
|
6763
|
+
* node, keyed by layout id (see {@link hyperbolicChildIds}). Positions never depend on the current focus —
|
|
6764
|
+
* refocusing is purely a Möbius re-projection of these fixed coordinates, so
|
|
6765
|
+
* the tree never needs to be re-laid-out when the focus changes.
|
|
6766
|
+
*
|
|
6767
|
+
* Each node holds an angular wedge — inherited from its parent, expressed in
|
|
6768
|
+
* its *own* local frame (root starts with the full `[-π, π]`) — across which
|
|
6769
|
+
* its children are placed at hyperbolic distance of at least `curvature`
|
|
6770
|
+
* (default `1`) — further for a thin slice, per `siblingSpacing` (see
|
|
6771
|
+
* {@link childDistance}).
|
|
6772
|
+
* That wedge is partitioned among the children, weighted by √ of each child's own
|
|
6773
|
+
* subtree size rather than split evenly, and each child's own share is
|
|
6774
|
+
* carried into its local frame via {@link boundaryAngleInLocalFrame} before
|
|
6775
|
+
* recursing. This is a strict partition, and each child's wedge region
|
|
6776
|
+
* (bounded by geodesic rays to the same two ideal boundary points as its
|
|
6777
|
+
* slice) nests inside its parent's, which is what guarantees sibling
|
|
6778
|
+
* subtrees never spatially cross regardless of how deep or bushy they are.
|
|
6779
|
+
*
|
|
6780
|
+
* Every node's local frame is tracked as a full {@link DiskIsometry}
|
|
6781
|
+
* (local → canonical), built by composing its parent's frame with the
|
|
6782
|
+
* translation to the child. Composing Möbius translations introduces a
|
|
6783
|
+
* rotation (gyration); dropping it — i.e. assuming a node's frame is simply
|
|
6784
|
+
* `mobiusToOrigin(·, position)` — makes every wedge from depth 3 onward
|
|
6785
|
+
* point in a subtly wrong direction, so subtrees cross each other.
|
|
6786
|
+
*
|
|
6787
|
+
* A wedge wider than π is narrowed to π (around the same bisector) before
|
|
6788
|
+
* the child recurses into it: a near-full-circle wedge would otherwise fan
|
|
6789
|
+
* that child's own children back around towards — and across — its parent.
|
|
6790
|
+
*
|
|
6791
|
+
* `curvature` scales how far apart (in hyperbolic distance) each generation
|
|
6792
|
+
* sits. Raising it pushes each generation further out, so the Poincaré
|
|
6793
|
+
* metric's own `tanh` compression crowds distant generations toward the
|
|
6794
|
+
* boundary faster — a stronger fisheye, which is why it reads as "zooming
|
|
6795
|
+
* in" on the focus. Lowering it spreads generations out more evenly,
|
|
6796
|
+
* flattening the view toward a Euclidean-feeling layout.
|
|
6797
|
+
*/
|
|
6798
|
+
function computeCanonicalPositions(root, maxDepth = 24, curvature = 1, siblingSpacing = DEFAULT_SIBLING_SPACING) {
|
|
6799
|
+
const map = /* @__PURE__ */ new Map();
|
|
6800
|
+
if (!root) return map;
|
|
6801
|
+
const origin = {
|
|
6802
|
+
x: 0,
|
|
6803
|
+
y: 0
|
|
6804
|
+
};
|
|
6805
|
+
map.set(root.item.key, {
|
|
6806
|
+
node: root,
|
|
6807
|
+
id: root.item.key,
|
|
6808
|
+
depth: 0,
|
|
6809
|
+
parentKey: void 0,
|
|
6810
|
+
parentId: void 0,
|
|
6811
|
+
position: origin,
|
|
6812
|
+
toParent: identityIsometry,
|
|
6813
|
+
angularShare: 2 * Math.PI
|
|
6814
|
+
});
|
|
6815
|
+
const place = (node, id, frame, wedgeStart, wedgeEnd, depth) => {
|
|
6816
|
+
if (depth >= maxDepth) return;
|
|
6817
|
+
const children = node.children ?? [];
|
|
6818
|
+
const n = children.length;
|
|
6819
|
+
if (n === 0) return;
|
|
6820
|
+
const angularWidth = wedgeEnd - wedgeStart;
|
|
6821
|
+
const weights = children.map((child) => Math.sqrt(subtreeSize(child, maxDepth - depth - 1)));
|
|
6822
|
+
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
6823
|
+
const ids = hyperbolicChildIds(id, children);
|
|
6824
|
+
let cursor = wedgeStart;
|
|
6825
|
+
children.forEach((child, index) => {
|
|
6826
|
+
const share = n === 1 ? angularWidth : angularWidth * weights[index] / totalWeight;
|
|
6827
|
+
const bisector = cursor + share / 2;
|
|
6828
|
+
cursor += share;
|
|
6829
|
+
const theta = bisector + (n === 1 ? stableUnitJitter(child.item.key) * Math.min(CHAIN_JITTER_RADIANS, share * .4) : 0);
|
|
6830
|
+
const r = euclideanRadiusForHyperbolicDistance(n === 1 ? curvature : childDistance(share, siblingSpacing, curvature));
|
|
6831
|
+
const local = {
|
|
6832
|
+
x: r * Math.cos(theta),
|
|
6833
|
+
y: r * Math.sin(theta)
|
|
6834
|
+
};
|
|
6835
|
+
const toParent = translationFromOrigin(local);
|
|
6836
|
+
const childFrame = composeIsometry(frame, toParent);
|
|
6837
|
+
map.set(ids[index], {
|
|
6838
|
+
node: child,
|
|
6839
|
+
id: ids[index],
|
|
6840
|
+
depth: depth + 1,
|
|
6841
|
+
parentKey: node.item.key,
|
|
6842
|
+
parentId: id,
|
|
6843
|
+
position: applyIsometry(childFrame, origin),
|
|
6844
|
+
toParent,
|
|
6845
|
+
angularShare: share
|
|
6846
|
+
});
|
|
6847
|
+
const halfWedge = Math.min(share, Math.PI) / 2;
|
|
6848
|
+
const localStart = boundaryAngleInLocalFrame(bisector - halfWedge, local);
|
|
6849
|
+
let localWidth = boundaryAngleInLocalFrame(bisector + halfWedge, local) - localStart;
|
|
6850
|
+
if (localWidth <= 0) localWidth += 2 * Math.PI;
|
|
6851
|
+
place(child, ids[index], childFrame, localStart, localStart + localWidth, depth + 1);
|
|
6852
|
+
});
|
|
6853
|
+
};
|
|
6854
|
+
place(root, root.item.key, identityIsometry, -Math.PI, Math.PI, 0);
|
|
6855
|
+
return map;
|
|
6856
|
+
}
|
|
6857
|
+
/**
|
|
6858
|
+
* Every node within `maxRadius` tree-graph steps of `focusedId`, with its
|
|
6859
|
+
* frame relative to the focus. Built by walking outward from the focus and
|
|
6860
|
+
* composing one short {@link CanonicalEntry.toParent} hop per edge — so
|
|
6861
|
+
* precision depends only on distance *from the focus*, never on how far the
|
|
6862
|
+
* focus is from the root.
|
|
6863
|
+
*/
|
|
6864
|
+
function focusFrames(entries, focusedId, maxRadius) {
|
|
6865
|
+
const frames = /* @__PURE__ */ new Map();
|
|
6866
|
+
if (!entries.has(focusedId)) return frames;
|
|
6867
|
+
frames.set(focusedId, {
|
|
6868
|
+
ring: 0,
|
|
6869
|
+
frame: identityIsometry
|
|
6870
|
+
});
|
|
6871
|
+
let frontier = [focusedId];
|
|
6872
|
+
for (let ring = 1; ring <= maxRadius && frontier.length > 0; ring++) {
|
|
6873
|
+
const next = [];
|
|
6874
|
+
for (const id of frontier) {
|
|
6875
|
+
const entry = entries.get(id);
|
|
6876
|
+
const { frame } = frames.get(id);
|
|
6877
|
+
const visit = (neighbourId, neighbourFrame) => {
|
|
6878
|
+
if (frames.has(neighbourId) || !entries.has(neighbourId)) return;
|
|
6879
|
+
frames.set(neighbourId, {
|
|
6880
|
+
ring,
|
|
6881
|
+
frame: neighbourFrame()
|
|
6882
|
+
});
|
|
6883
|
+
next.push(neighbourId);
|
|
6884
|
+
};
|
|
6885
|
+
for (const childId of hyperbolicChildIds(id, entry.node.children ?? [])) visit(childId, () => composeIsometry(frame, entries.get(childId).toParent));
|
|
6886
|
+
if (entry.parentId !== void 0) visit(entry.parentId, () => composeIsometry(frame, invertIsometry(entry.toParent)));
|
|
6887
|
+
}
|
|
6888
|
+
frontier = next;
|
|
6889
|
+
}
|
|
6890
|
+
return frames;
|
|
6891
|
+
}
|
|
6892
|
+
/** `toParent` hops composed from `id` up to (not including) ancestor `ancestorId`: `id`-local → `ancestorId`-local. */
|
|
6893
|
+
function frameUpTo(entries, id, ancestorId) {
|
|
6894
|
+
let frame = identityIsometry;
|
|
6895
|
+
for (let cursor = id; cursor !== void 0 && cursor !== ancestorId; cursor = entries.get(cursor)?.parentId) frame = composeIsometry(entries.get(cursor).toParent, frame);
|
|
6896
|
+
return frame;
|
|
6897
|
+
}
|
|
6898
|
+
/**
|
|
6899
|
+
* The frame of node `toId` expressed in node `fromId`'s local frame (maps
|
|
6900
|
+
* `toId`-local coordinates to `fromId`-local ones), composed along the tree
|
|
6901
|
+
* path between them via their lowest common ancestor. Used to hand a view
|
|
6902
|
+
* over from one focus to another without passing through root-anchored
|
|
6903
|
+
* coordinates. `undefined` if either id is absent.
|
|
6904
|
+
*/
|
|
6905
|
+
function hyperbolicRelativeFrame(entries, fromId, toId) {
|
|
6906
|
+
if (!entries.has(fromId) || !entries.has(toId)) return void 0;
|
|
6907
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
6908
|
+
for (let cursor = fromId; cursor !== void 0; cursor = entries.get(cursor)?.parentId) ancestors.add(cursor);
|
|
6909
|
+
let lca = toId;
|
|
6910
|
+
while (lca !== void 0 && !ancestors.has(lca)) lca = entries.get(lca)?.parentId;
|
|
6911
|
+
if (lca === void 0) return void 0;
|
|
6912
|
+
return composeIsometry(invertIsometry(frameUpTo(entries, fromId, lca)), frameUpTo(entries, toId, lca));
|
|
6913
|
+
}
|
|
6914
|
+
/**
|
|
6915
|
+
* Resolve `focusedKey` — either a layout id, or (as a convenience, and for
|
|
6916
|
+
* data with globally-unique keys) a plain `item.key`, matching the first
|
|
6917
|
+
* node in depth-first order carrying it.
|
|
6918
|
+
*/
|
|
6919
|
+
function resolveFocusId(canonical, focusedKey) {
|
|
6920
|
+
if (focusedKey === void 0) return void 0;
|
|
6921
|
+
if (canonical.has(focusedKey)) return focusedKey;
|
|
6922
|
+
for (const entry of canonical.values()) if (entry.node.item.key === focusedKey) return entry.id;
|
|
6923
|
+
}
|
|
6924
|
+
/**
|
|
6925
|
+
* Compute the visible nodes/edges of a hyperbolic tree view: every
|
|
6926
|
+
* currently-loaded node within `maxRadius` tree-graph steps of the focused
|
|
6927
|
+
* node, projected onto the Poincaré disk with the focus at its center.
|
|
6928
|
+
*
|
|
6929
|
+
* @param root True tree root. `undefined` yields an empty layout.
|
|
6930
|
+
* @param options `focusedKey` (a layout id or plain `item.key`; default the
|
|
6931
|
+
* true root; unknown keys fall back to the true root), `maxRadius` (default unlimited), `maxDepth` (safety
|
|
6932
|
+
* cap on layout recursion depth, default 24), `curvature`,
|
|
6933
|
+
* `siblingSpacing`, `centerOverride` (put this point of the focus's local
|
|
6934
|
+
* frame at the center instead of the focus itself), `view` (a full
|
|
6935
|
+
* focus-local → screen isometry, taking precedence over `centerOverride` —
|
|
6936
|
+
* used for panning and animation, which also rotate).
|
|
6937
|
+
*/
|
|
6938
|
+
function computeHyperbolicLayout(root, options = {}) {
|
|
6939
|
+
if (!root) return {
|
|
6940
|
+
nodes: [],
|
|
6941
|
+
edges: []
|
|
6942
|
+
};
|
|
6943
|
+
const { maxRadius = Number.POSITIVE_INFINITY, maxDepth = 24, curvature = 1, siblingSpacing = DEFAULT_SIBLING_SPACING } = options;
|
|
6944
|
+
const canonical = computeCanonicalPositions(root, maxDepth, curvature, siblingSpacing);
|
|
6945
|
+
const focusedId = resolveFocusId(canonical, options.focusedKey) ?? root.item.key;
|
|
6946
|
+
if (!canonical.has(focusedId)) return {
|
|
6947
|
+
nodes: [],
|
|
6948
|
+
edges: []
|
|
6949
|
+
};
|
|
6950
|
+
const view = options.view ?? viewIsometry(options.centerOverride ?? {
|
|
6951
|
+
x: 0,
|
|
6952
|
+
y: 0
|
|
6953
|
+
});
|
|
6954
|
+
const frames = focusFrames(canonical, focusedId, maxRadius);
|
|
6955
|
+
const rawWeights = [...frames.keys()].map((key) => canonical.get(key).node.item.weight).filter((w) => typeof w === `number`);
|
|
6029
6956
|
const minWeight = rawWeights.length > 0 ? Math.min(...rawWeights) : void 0;
|
|
6030
6957
|
const maxWeight = rawWeights.length > 0 ? Math.max(...rawWeights) : void 0;
|
|
6031
6958
|
const normalizeWeight = (weight) => {
|
|
6032
6959
|
if (weight === void 0 || minWeight === void 0 || maxWeight === void 0 || maxWeight === minWeight) return .5;
|
|
6033
6960
|
return (weight - minWeight) / (maxWeight - minWeight);
|
|
6034
6961
|
};
|
|
6035
|
-
const nodes = [...
|
|
6036
|
-
const entry = canonical.get(
|
|
6962
|
+
const nodes = [...frames.entries()].map(([id, { ring, frame }]) => {
|
|
6963
|
+
const entry = canonical.get(id);
|
|
6037
6964
|
const weight = entry.node.item.weight;
|
|
6038
6965
|
return {
|
|
6039
6966
|
node: entry.node,
|
|
6040
|
-
key,
|
|
6967
|
+
key: entry.node.item.key,
|
|
6968
|
+
id,
|
|
6041
6969
|
depth: entry.depth,
|
|
6042
|
-
ring
|
|
6970
|
+
ring,
|
|
6043
6971
|
parentKey: entry.parentKey,
|
|
6044
|
-
|
|
6972
|
+
parentId: entry.parentId,
|
|
6973
|
+
position: applyIsometry(composeIsometry(view, frame), {
|
|
6974
|
+
x: 0,
|
|
6975
|
+
y: 0
|
|
6976
|
+
}),
|
|
6045
6977
|
angularShare: entry.angularShare,
|
|
6046
6978
|
weight,
|
|
6047
6979
|
weightNormalized: normalizeWeight(weight)
|
|
6048
6980
|
};
|
|
6049
6981
|
}).sort((a, b) => a.ring - b.ring);
|
|
6050
|
-
const
|
|
6982
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
6051
6983
|
const edges = [];
|
|
6052
6984
|
for (const n of nodes) {
|
|
6053
|
-
if (n.
|
|
6054
|
-
const parent =
|
|
6985
|
+
if (n.parentId === void 0) continue;
|
|
6986
|
+
const parent = byId.get(n.parentId);
|
|
6055
6987
|
if (!parent) continue;
|
|
6056
6988
|
edges.push({
|
|
6057
6989
|
fromKey: parent.key,
|
|
6058
6990
|
toKey: n.key,
|
|
6991
|
+
fromId: parent.id,
|
|
6992
|
+
toId: n.id,
|
|
6059
6993
|
from: parent.position,
|
|
6060
6994
|
to: n.position,
|
|
6061
6995
|
weightNormalized: (parent.weightNormalized + n.weightNormalized) / 2
|
|
@@ -6066,18 +7000,477 @@ function computeHyperbolicLayout(root, options = {}) {
|
|
|
6066
7000
|
edges
|
|
6067
7001
|
};
|
|
6068
7002
|
}
|
|
6069
|
-
//#endregion
|
|
6070
|
-
//#region src/hyperbolic-tree/
|
|
7003
|
+
//#endregion
|
|
7004
|
+
//#region src/hyperbolic-tree/keyboard.ts
|
|
7005
|
+
const unhandled = {
|
|
7006
|
+
action: { kind: `none` },
|
|
7007
|
+
handled: false
|
|
7008
|
+
};
|
|
7009
|
+
/** Angular distance (radians, 0–π) of a layout node from screen-"up" (−π/2). */
|
|
7010
|
+
function angleDistanceFromUp(n) {
|
|
7011
|
+
const angle = Math.atan2(n.position.y, n.position.x);
|
|
7012
|
+
let diff = Math.abs(angle - -Math.PI / 2);
|
|
7013
|
+
if (diff > Math.PI) diff = 2 * Math.PI - diff;
|
|
7014
|
+
return diff;
|
|
7015
|
+
}
|
|
7016
|
+
/**
|
|
7017
|
+
* Resolve `key` against the focused node (layout id `focusId`):
|
|
7018
|
+
* - `ArrowUp` — the visible child closest to screen-"up"
|
|
7019
|
+
* - `ArrowDown` — the parent
|
|
7020
|
+
* - `ArrowLeft` / `ArrowRight` — previous / next sibling, wrapping
|
|
7021
|
+
* - `Enter` — activate the focus
|
|
7022
|
+
* - `Escape` — back to the root (unhandled when already there)
|
|
7023
|
+
*/
|
|
7024
|
+
function resolveKeyboardAction(key, root, focusId, visibleNodes) {
|
|
7025
|
+
const path = findHyperbolicPathById(root, focusId);
|
|
7026
|
+
const focusNode = path?.at(-1);
|
|
7027
|
+
if (!path || !focusNode) return unhandled;
|
|
7028
|
+
const parent = path.length >= 2 ? path[path.length - 2] : void 0;
|
|
7029
|
+
const none = {
|
|
7030
|
+
action: { kind: `none` },
|
|
7031
|
+
handled: true
|
|
7032
|
+
};
|
|
7033
|
+
const focus = (node) => node ? {
|
|
7034
|
+
action: {
|
|
7035
|
+
kind: `focus`,
|
|
7036
|
+
node
|
|
7037
|
+
},
|
|
7038
|
+
handled: true
|
|
7039
|
+
} : none;
|
|
7040
|
+
switch (key) {
|
|
7041
|
+
case `ArrowUp`: {
|
|
7042
|
+
const children = visibleNodes.filter((n) => n.parentId === focusId);
|
|
7043
|
+
if (children.length === 0) return none;
|
|
7044
|
+
return focus(children.reduce((a, b) => angleDistanceFromUp(a) < angleDistanceFromUp(b) ? a : b).node);
|
|
7045
|
+
}
|
|
7046
|
+
case `ArrowDown`: return focus(parent);
|
|
7047
|
+
case `ArrowLeft`:
|
|
7048
|
+
case `ArrowRight`: {
|
|
7049
|
+
const siblings = parent?.children ?? [];
|
|
7050
|
+
const index = siblings.indexOf(focusNode);
|
|
7051
|
+
if (index === -1 || siblings.length <= 1) return none;
|
|
7052
|
+
return focus(siblings[(index + (key === `ArrowRight` ? 1 : -1) + siblings.length) % siblings.length]);
|
|
7053
|
+
}
|
|
7054
|
+
case `Enter`: return {
|
|
7055
|
+
action: {
|
|
7056
|
+
kind: `activate`,
|
|
7057
|
+
node: focusNode,
|
|
7058
|
+
depth: path.length - 1
|
|
7059
|
+
},
|
|
7060
|
+
handled: true
|
|
7061
|
+
};
|
|
7062
|
+
case `Escape`: return focusNode === root ? unhandled : focus(root);
|
|
7063
|
+
default: return unhandled;
|
|
7064
|
+
}
|
|
7065
|
+
}
|
|
7066
|
+
//#endregion
|
|
7067
|
+
//#region src/hyperbolic-tree/lazy-children.ts
|
|
7068
|
+
function createLazyChildLoader(source, onChange) {
|
|
7069
|
+
const pending = /* @__PURE__ */ new Set();
|
|
7070
|
+
const aborters = /* @__PURE__ */ new Map();
|
|
7071
|
+
const fetchChildren = async (node, depth, signal) => {
|
|
7072
|
+
const { model, loadChildren } = source();
|
|
7073
|
+
if (model) return model.getChildren(node, depth, signal);
|
|
7074
|
+
return [...await collectDataResult(loadChildren({
|
|
7075
|
+
node,
|
|
7076
|
+
depth
|
|
7077
|
+
}, signal), signal)];
|
|
7078
|
+
};
|
|
7079
|
+
const load = async (node, depth) => {
|
|
7080
|
+
pending.add(node);
|
|
7081
|
+
const controller = new AbortController();
|
|
7082
|
+
aborters.set(node, controller);
|
|
7083
|
+
onChange();
|
|
7084
|
+
try {
|
|
7085
|
+
const children = await fetchChildren(node, depth, controller.signal);
|
|
7086
|
+
if (!controller.signal.aborted) node.children = children;
|
|
7087
|
+
} catch {
|
|
7088
|
+
if (!controller.signal.aborted) node.children = [];
|
|
7089
|
+
} finally {
|
|
7090
|
+
pending.delete(node);
|
|
7091
|
+
aborters.delete(node);
|
|
7092
|
+
if (!controller.signal.aborted) onChange();
|
|
7093
|
+
}
|
|
7094
|
+
};
|
|
7095
|
+
return {
|
|
7096
|
+
/** Start loading children for every node in `visible` that has none yet. */
|
|
7097
|
+
ensure(visible) {
|
|
7098
|
+
const { model, loadChildren } = source();
|
|
7099
|
+
if (!model && !loadChildren) return;
|
|
7100
|
+
for (const { node, depth } of visible) if (node.children === void 0 && !pending.has(node)) load(node, depth);
|
|
7101
|
+
},
|
|
7102
|
+
isPending: (node) => pending.has(node),
|
|
7103
|
+
abortAll() {
|
|
7104
|
+
for (const aborter of aborters.values()) aborter.abort();
|
|
7105
|
+
aborters.clear();
|
|
7106
|
+
pending.clear();
|
|
7107
|
+
}
|
|
7108
|
+
};
|
|
7109
|
+
}
|
|
7110
|
+
//#endregion
|
|
7111
|
+
//#region src/hyperbolic-tree/render-utils.ts
|
|
7112
|
+
/**
|
|
7113
|
+
* The Poincaré disk's conformal factor relative to the origin, `1 − |p|²`:
|
|
7114
|
+
* how much a shape of fixed hyperbolic size shrinks (in Euclidean terms) when
|
|
7115
|
+
* moved from the center to `p`. Sizing nodes by this makes them shrink
|
|
7116
|
+
* exactly as the space does, rather than by an ad-hoc falloff.
|
|
7117
|
+
*/
|
|
7118
|
+
function conformalScale(p) {
|
|
7119
|
+
return Math.max(0, 1 - (p.x * p.x + p.y * p.y));
|
|
7120
|
+
}
|
|
7121
|
+
/**
|
|
7122
|
+
* Euclidean distance from each node to its nearest visible neighbour, keyed
|
|
7123
|
+
* by layout id (`Infinity` for a lone node). Used to cap on-screen node size
|
|
7124
|
+
* so crowded nodes never overlap — measured in the *projected* view, so it
|
|
7125
|
+
* stays right whichever node is focused. O(n²), fine for the few hundred
|
|
7126
|
+
* nodes a `maxRadius` window typically holds.
|
|
7127
|
+
*/
|
|
7128
|
+
function nearestNeighbourDistances(nodes) {
|
|
7129
|
+
const out = /* @__PURE__ */ new Map();
|
|
7130
|
+
for (const n of nodes) out.set(n.id, Number.POSITIVE_INFINITY);
|
|
7131
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
7132
|
+
const a = nodes[i].position;
|
|
7133
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
7134
|
+
const b = nodes[j].position;
|
|
7135
|
+
const d = Math.hypot(a.x - b.x, a.y - b.y);
|
|
7136
|
+
if (d < out.get(nodes[i].id)) out.set(nodes[i].id, d);
|
|
7137
|
+
if (d < out.get(nodes[j].id)) out.set(nodes[j].id, d);
|
|
7138
|
+
}
|
|
7139
|
+
}
|
|
7140
|
+
return out;
|
|
7141
|
+
}
|
|
7142
|
+
/** Weight multiplier for `n`, interpolating between the min/max weight scales. */
|
|
7143
|
+
function weightScale(n, options) {
|
|
7144
|
+
return options.minWeightScale + (options.maxWeightScale - options.minWeightScale) * n.weightNormalized;
|
|
7145
|
+
}
|
|
7146
|
+
/**
|
|
7147
|
+
* A dot's rendered radius: base size × weight × conformal shrink, capped to
|
|
7148
|
+
* under half the distance to its nearest neighbour so crowded siblings keep
|
|
7149
|
+
* a visible gap instead of merging into a blob.
|
|
7150
|
+
*/
|
|
7151
|
+
function dotRadius(n, nearest, options) {
|
|
7152
|
+
const radius = options.nodeRadius * conformalScale(n.position) * weightScale(n, options);
|
|
7153
|
+
return Math.max(Math.min(radius, nearest * .45), .003);
|
|
7154
|
+
}
|
|
7155
|
+
/**
|
|
7156
|
+
* Footprint of a `2·halfWidth × 2·halfHeight` rectangle centered on
|
|
7157
|
+
* `center` and rotated by `angle` radians.
|
|
7158
|
+
*/
|
|
7159
|
+
function orientedRect(id, center, halfWidth, halfHeight, angle = 0) {
|
|
7160
|
+
const cos = Math.cos(angle);
|
|
7161
|
+
const sin = Math.sin(angle);
|
|
7162
|
+
const corner = (u, v) => ({
|
|
7163
|
+
x: center.x + u * cos - v * sin,
|
|
7164
|
+
y: center.y + u * sin + v * cos
|
|
7165
|
+
});
|
|
7166
|
+
return {
|
|
7167
|
+
id,
|
|
7168
|
+
corners: [
|
|
7169
|
+
corner(-halfWidth, -halfHeight),
|
|
7170
|
+
corner(halfWidth, -halfHeight),
|
|
7171
|
+
corner(halfWidth, halfHeight),
|
|
7172
|
+
corner(-halfWidth, halfHeight)
|
|
7173
|
+
]
|
|
7174
|
+
};
|
|
7175
|
+
}
|
|
7176
|
+
function polygonArea(points) {
|
|
7177
|
+
let area = 0;
|
|
7178
|
+
for (let i = 0; i < points.length; i++) {
|
|
7179
|
+
const a = points[i];
|
|
7180
|
+
const b = points[(i + 1) % points.length];
|
|
7181
|
+
area += a.x * b.y - b.x * a.y;
|
|
7182
|
+
}
|
|
7183
|
+
return Math.abs(area) / 2;
|
|
7184
|
+
}
|
|
7185
|
+
/** Sutherland–Hodgman: `subject` clipped to convex `clip` (either winding). */
|
|
7186
|
+
function clipPolygon(subject, clip) {
|
|
7187
|
+
const orientation = Math.sign((clip[1].x - clip[0].x) * (clip[2].y - clip[0].y) - (clip[1].y - clip[0].y) * (clip[2].x - clip[0].x)) || 1;
|
|
7188
|
+
let output = [...subject];
|
|
7189
|
+
for (let i = 0; i < clip.length && output.length > 0; i++) {
|
|
7190
|
+
const a = clip[i];
|
|
7191
|
+
const b = clip[(i + 1) % clip.length];
|
|
7192
|
+
const side = (p) => orientation * ((b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x));
|
|
7193
|
+
const input = output;
|
|
7194
|
+
output = [];
|
|
7195
|
+
for (let j = 0; j < input.length; j++) {
|
|
7196
|
+
const current = input[j];
|
|
7197
|
+
const previous = input[(j + input.length - 1) % input.length];
|
|
7198
|
+
const sc = side(current);
|
|
7199
|
+
const sp = side(previous);
|
|
7200
|
+
if (sc >= 0 !== sp >= 0) {
|
|
7201
|
+
const t = sp / (sp - sc);
|
|
7202
|
+
output.push({
|
|
7203
|
+
x: previous.x + (current.x - previous.x) * t,
|
|
7204
|
+
y: previous.y + (current.y - previous.y) * t
|
|
7205
|
+
});
|
|
7206
|
+
}
|
|
7207
|
+
if (sc >= 0) output.push(current);
|
|
7208
|
+
}
|
|
7209
|
+
}
|
|
7210
|
+
return output;
|
|
7211
|
+
}
|
|
7212
|
+
function bounds(points) {
|
|
7213
|
+
const xs = points.map((p) => p.x);
|
|
7214
|
+
const ys = points.map((p) => p.y);
|
|
7215
|
+
return {
|
|
7216
|
+
x0: Math.min(...xs),
|
|
7217
|
+
y0: Math.min(...ys),
|
|
7218
|
+
x1: Math.max(...xs),
|
|
7219
|
+
y1: Math.max(...ys)
|
|
7220
|
+
};
|
|
7221
|
+
}
|
|
7222
|
+
/** Fraction (0–1) of `candidate`'s area covered by `other`. */
|
|
7223
|
+
function overlapFraction(candidate, other) {
|
|
7224
|
+
const a = bounds(candidate.corners);
|
|
7225
|
+
const b = bounds(other.corners);
|
|
7226
|
+
if (a.x1 <= b.x0 || b.x1 <= a.x0 || a.y1 <= b.y0 || b.y1 <= a.y0) return 0;
|
|
7227
|
+
const area = polygonArea(candidate.corners);
|
|
7228
|
+
if (area <= 0) return 1;
|
|
7229
|
+
return polygonArea(clipPolygon(candidate.corners, other.corners)) / area;
|
|
7230
|
+
}
|
|
7231
|
+
/**
|
|
7232
|
+
* Greedy label placement: walk `shapes` in priority order and keep each one
|
|
7233
|
+
* whose overlap with every already-kept shape is at most `maxOverlap` of its
|
|
7234
|
+
* own area. Returns the ids of the kept labels. Checking actual (possibly
|
|
7235
|
+
* rotated) label footprints, not just node positions, is what stops a fan of
|
|
7236
|
+
* same-radius siblings from stacking their text into an unreadable pile.
|
|
7237
|
+
*/
|
|
7238
|
+
function cullOverlappingLabels(shapes, maxOverlap = .03) {
|
|
7239
|
+
const kept = [];
|
|
7240
|
+
for (const shape of shapes) if (kept.every((k) => overlapFraction(shape, k) <= maxOverlap)) kept.push(shape);
|
|
7241
|
+
return new Set(kept.map((r) => r.id));
|
|
7242
|
+
}
|
|
7243
|
+
/** `label` cut to at most `maxChars` characters, ending in `…` when cut. `0` or less disables truncation. */
|
|
7244
|
+
function truncateLabel(label, maxChars) {
|
|
7245
|
+
if (maxChars <= 0 || label.length <= maxChars) return label;
|
|
7246
|
+
return `${label.slice(0, Math.max(1, maxChars - 1)).trimEnd()}…`;
|
|
7247
|
+
}
|
|
7248
|
+
let measureContext;
|
|
7249
|
+
const widthCache = /* @__PURE__ */ new Map();
|
|
7250
|
+
/**
|
|
7251
|
+
* Rendered width of `text` at `fontSize` px in `fontFamily`, via an offscreen
|
|
7252
|
+
* canvas (cached). Falls back to a per-character estimate where canvas text
|
|
7253
|
+
* measurement is unavailable (e.g. non-browser test environments).
|
|
7254
|
+
*/
|
|
7255
|
+
function measureTextWidth(text, fontSize, fontFamily) {
|
|
7256
|
+
const cacheKey = `${fontFamily}\u0000${text}`;
|
|
7257
|
+
let atUnitSize = widthCache.get(cacheKey);
|
|
7258
|
+
if (atUnitSize === void 0) {
|
|
7259
|
+
if (measureContext === void 0) try {
|
|
7260
|
+
measureContext = typeof document === `undefined` ? null : document.createElement(`canvas`).getContext(`2d`);
|
|
7261
|
+
} catch {
|
|
7262
|
+
measureContext = null;
|
|
7263
|
+
}
|
|
7264
|
+
const reference = 100;
|
|
7265
|
+
let measured;
|
|
7266
|
+
if (measureContext) {
|
|
7267
|
+
measureContext.font = `${reference}px ${fontFamily || `sans-serif`}`;
|
|
7268
|
+
const width = measureContext.measureText(text).width;
|
|
7269
|
+
if (Number.isFinite(width) && width > 0) measured = width / reference;
|
|
7270
|
+
}
|
|
7271
|
+
atUnitSize = measured ?? text.length * .6;
|
|
7272
|
+
widthCache.set(cacheKey, atUnitSize);
|
|
7273
|
+
}
|
|
7274
|
+
return atUnitSize * fontSize;
|
|
7275
|
+
}
|
|
7276
|
+
/**
|
|
7277
|
+
* Smallest scale a box shrinks to, however far from the focus. Pure conformal
|
|
7278
|
+
* scaling (`1 − |z|²`) is geometrically exact but shrinks the second ring to
|
|
7279
|
+
* well under half size, taking labels below legibility long before the
|
|
7280
|
+
* boundary — flooring it keeps nearby rings readable while still reading as
|
|
7281
|
+
* "further away".
|
|
7282
|
+
*/
|
|
7283
|
+
const BOX_SCALE_FLOOR = .45;
|
|
7284
|
+
/**
|
|
7285
|
+
* Geometry for a `box`-shaped node. The box is laid out at its focus size and
|
|
7286
|
+
* then uniformly scaled — by the conformal factor, floored at
|
|
7287
|
+
* {@link BOX_SCALE_FLOOR} — so the label is measured just once. Weight
|
|
7288
|
+
* nudges the scale gently (square-rooted), so heavy nodes read as larger
|
|
7289
|
+
* without dwarfing their neighbours' labels.
|
|
7290
|
+
*/
|
|
7291
|
+
function boxGeometry(n, options) {
|
|
7292
|
+
const conformal = BOX_SCALE_FLOOR + .55 * conformalScale(n.position);
|
|
7293
|
+
const scale = Math.min(1, conformal * (options.zoom ?? 1)) * Math.sqrt(weightScale(n, options));
|
|
7294
|
+
const text = truncateLabel(n.node.item.label, options.maxChars);
|
|
7295
|
+
const padX = options.fontSize * .5;
|
|
7296
|
+
const height = options.fontSize * 1.55;
|
|
7297
|
+
return {
|
|
7298
|
+
scale,
|
|
7299
|
+
width: measureTextWidth(text, options.fontSize, options.fontFamily) + padX * 2,
|
|
7300
|
+
height,
|
|
7301
|
+
markerSize: options.fontSize * .55,
|
|
7302
|
+
text
|
|
7303
|
+
};
|
|
7304
|
+
}
|
|
7305
|
+
/** Label footprint of a box of `geometry` centered at `center` and rotated by `angle` (render units). */
|
|
7306
|
+
function boxLabelShape(id, center, geometry, angle = 0) {
|
|
7307
|
+
return orientedRect(id, center, geometry.width * geometry.scale / 2, geometry.height * geometry.scale / 2, angle);
|
|
7308
|
+
}
|
|
7309
|
+
//#endregion
|
|
7310
|
+
//#region src/hyperbolic-tree/render-context.ts
|
|
7311
|
+
const HORIZONTAL = {
|
|
7312
|
+
direction: 0,
|
|
7313
|
+
textAngle: 0,
|
|
7314
|
+
flipped: false
|
|
7315
|
+
};
|
|
7316
|
+
/**
|
|
7317
|
+
* Label font metrics. `--hyperbolictree-font-size` is in real screen px;
|
|
7318
|
+
* the SVG's viewBox is fixed, so it's converted to render units using the
|
|
7319
|
+
* on-screen disk size (`extentPx`) and zoom — keeping text legible whatever
|
|
7320
|
+
* size the element is laid out at, and constant-size while zooming
|
|
7321
|
+
* (zooming in spreads crowded labels apart rather than just magnifying the
|
|
7322
|
+
* pile).
|
|
7323
|
+
*/
|
|
7324
|
+
function fontMetrics(host, extentPx, zoom) {
|
|
7325
|
+
const style = typeof getComputedStyle === `function` ? getComputedStyle(host) : void 0;
|
|
7326
|
+
const parsed = Number.parseFloat(style?.getPropertyValue(`--hyperbolictree-font-size`) ?? ``);
|
|
7327
|
+
const px = Number.isFinite(parsed) && parsed > 0 ? parsed : 13;
|
|
7328
|
+
return {
|
|
7329
|
+
px,
|
|
7330
|
+
units: px * (1050 / (extentPx > 0 ? extentPx : 420) / (zoom || 1)),
|
|
7331
|
+
family: style?.fontFamily ?? ``
|
|
7332
|
+
};
|
|
7333
|
+
}
|
|
7334
|
+
/** Map of top-level-branch layout id → index among the root's children. */
|
|
7335
|
+
function branchIndices(root) {
|
|
7336
|
+
if (!root) return /* @__PURE__ */ new Map();
|
|
7337
|
+
return new Map(hyperbolicChildIds(root.item.key, root.children ?? []).map((id, i) => [id, i]));
|
|
7338
|
+
}
|
|
7339
|
+
/** Inline `--branch-index`/`--branch-count` declarations for the node/edge with layout id `id` (empty for the root). */
|
|
7340
|
+
function branchStyle(id, branches) {
|
|
7341
|
+
const branch = hyperbolicBranchId(id);
|
|
7342
|
+
const index = branch === void 0 ? void 0 : branches.get(branch);
|
|
7343
|
+
return index === void 0 ? `` : ` --branch-index: ${index}; --branch-count: ${branches.size};`;
|
|
7344
|
+
}
|
|
7345
|
+
function groupSiblings(nodes) {
|
|
7346
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7347
|
+
for (const n of nodes) {
|
|
7348
|
+
const group = groups.get(n.parentId) ?? [];
|
|
7349
|
+
group.push(n);
|
|
7350
|
+
groups.set(n.parentId, group);
|
|
7351
|
+
}
|
|
7352
|
+
return groups;
|
|
7353
|
+
}
|
|
7354
|
+
/**
|
|
7355
|
+
* Label priority: the focus, then selected nodes, then larger (nearer the
|
|
7356
|
+
* center) first.
|
|
7357
|
+
*/
|
|
7358
|
+
function byLabelPriority(nodes, sizeOf, isSelected) {
|
|
7359
|
+
const rank = (n) => n.ring === 0 ? 0 : isSelected(n.node) ? 1 : 2;
|
|
7360
|
+
return [...nodes].sort((a, b) => rank(a) - rank(b) || sizeOf(b) - sizeOf(a) || a.ring - b.ring);
|
|
7361
|
+
}
|
|
7362
|
+
/**
|
|
7363
|
+
* Each node's label orientation. The reference neighbour is the one a step
|
|
7364
|
+
* nearer the focus: usually the parent, but for the focus's own ancestors
|
|
7365
|
+
* (which sit *behind* it) the child on the path back to the focus. The focus
|
|
7366
|
+
* itself stays horizontal.
|
|
7367
|
+
*/
|
|
7368
|
+
function labelOrientations(nodes, radial) {
|
|
7369
|
+
const out = /* @__PURE__ */ new Map();
|
|
7370
|
+
if (!radial) {
|
|
7371
|
+
for (const n of nodes) out.set(n.id, HORIZONTAL);
|
|
7372
|
+
return out;
|
|
7373
|
+
}
|
|
7374
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
7375
|
+
const nearer = /* @__PURE__ */ new Map();
|
|
7376
|
+
for (const n of nodes) {
|
|
7377
|
+
const parent = n.parentId === void 0 ? void 0 : byId.get(n.parentId);
|
|
7378
|
+
if (!parent) continue;
|
|
7379
|
+
if (parent.ring < n.ring) nearer.set(n.id, parent);
|
|
7380
|
+
else if (n.ring < parent.ring) nearer.set(parent.id, n);
|
|
7381
|
+
}
|
|
7382
|
+
for (const n of nodes) {
|
|
7383
|
+
const from = nearer.get(n.id);
|
|
7384
|
+
if (!from || n.ring === 0) {
|
|
7385
|
+
out.set(n.id, HORIZONTAL);
|
|
7386
|
+
continue;
|
|
7387
|
+
}
|
|
7388
|
+
const direction = Math.atan2(n.position.y - from.position.y, n.position.x - from.position.x);
|
|
7389
|
+
const flipped = Math.cos(direction) < 0;
|
|
7390
|
+
out.set(n.id, {
|
|
7391
|
+
direction,
|
|
7392
|
+
textAngle: flipped ? direction + Math.PI : direction,
|
|
7393
|
+
flipped
|
|
7394
|
+
});
|
|
7395
|
+
}
|
|
7396
|
+
return out;
|
|
7397
|
+
}
|
|
7398
|
+
const renderCenter = (n) => ({
|
|
7399
|
+
x: n.position.x * 500,
|
|
7400
|
+
y: n.position.y * 500
|
|
7401
|
+
});
|
|
7402
|
+
function boxLabels(nodes, options, orientations) {
|
|
7403
|
+
const { font } = options;
|
|
7404
|
+
const boxes = new Map(nodes.map((n) => [n.id, boxGeometry(n, {
|
|
7405
|
+
fontSize: font.units,
|
|
7406
|
+
fontFamily: font.family,
|
|
7407
|
+
maxChars: options.boxMaxChars,
|
|
7408
|
+
minWeightScale: options.minWeightScale,
|
|
7409
|
+
maxWeightScale: options.maxWeightScale,
|
|
7410
|
+
zoom: options.zoom
|
|
7411
|
+
})]));
|
|
7412
|
+
return {
|
|
7413
|
+
boxes,
|
|
7414
|
+
labelIds: cullOverlappingLabels(byLabelPriority(nodes.filter((n) => !options.isTickled(n.id) && font.px * boxes.get(n.id).scale >= options.minLabelSize), (n) => boxes.get(n.id).scale, options.isSelected).map((n) => boxLabelShape(n.id, renderCenter(n), boxes.get(n.id), orientations.get(n.id).textAngle)))
|
|
7415
|
+
};
|
|
7416
|
+
}
|
|
7417
|
+
function dotLabels(nodes, options, orientations) {
|
|
7418
|
+
const { font } = options;
|
|
7419
|
+
const nearest = nearestNeighbourDistances(nodes);
|
|
7420
|
+
const radii = new Map(nodes.map((n) => [n.id, dotRadius(n, nearest.get(n.id), options)]));
|
|
7421
|
+
return {
|
|
7422
|
+
radii,
|
|
7423
|
+
labelIds: cullOverlappingLabels(byLabelPriority(nodes.filter((n) => !options.isTickled(n.id) && radii.get(n.id) * options.zoom >= options.minNodeRadius), (n) => radii.get(n.id), options.isSelected).map((n) => {
|
|
7424
|
+
const { direction } = orientations.get(n.id);
|
|
7425
|
+
const halfWidth = measureTextWidth(n.node.item.label, font.units, font.family) / 2;
|
|
7426
|
+
const along = (radii.get(n.id) + .01) * 500 + halfWidth;
|
|
7427
|
+
const c = renderCenter(n);
|
|
7428
|
+
const center = {
|
|
7429
|
+
x: c.x + Math.cos(direction) * along,
|
|
7430
|
+
y: c.y + Math.sin(direction) * along
|
|
7431
|
+
};
|
|
7432
|
+
return orientedRect(n.id, center, halfWidth + font.units * .6, font.units * .6, direction);
|
|
7433
|
+
}))
|
|
7434
|
+
};
|
|
7435
|
+
}
|
|
7436
|
+
/**
|
|
7437
|
+
* Everything the node templates need for one render. Labels are placed
|
|
7438
|
+
* greedily by priority and dropped where they'd overlap; hovered/cursor
|
|
7439
|
+
* nodes are exempt from culling entirely — always labelled — so hovering a
|
|
7440
|
+
* marker reveals it without reshuffling everyone else's labels.
|
|
7441
|
+
*/
|
|
7442
|
+
function buildNodeRenderContext(nodes, options) {
|
|
7443
|
+
const orientations = labelOrientations(nodes, options.radialLabels);
|
|
7444
|
+
const shapeData = options.nodeShape === `box` ? {
|
|
7445
|
+
...boxLabels(nodes, options, orientations),
|
|
7446
|
+
radii: /* @__PURE__ */ new Map()
|
|
7447
|
+
} : {
|
|
7448
|
+
...dotLabels(nodes, options, orientations),
|
|
7449
|
+
boxes: /* @__PURE__ */ new Map()
|
|
7450
|
+
};
|
|
7451
|
+
const labelIds = new Set(shapeData.labelIds);
|
|
7452
|
+
for (const n of nodes) if (options.isTickled(n.id)) labelIds.add(n.id);
|
|
7453
|
+
return {
|
|
7454
|
+
siblingGroups: groupSiblings(nodes),
|
|
7455
|
+
font: options.font,
|
|
7456
|
+
branches: options.branches,
|
|
7457
|
+
boxes: shapeData.boxes,
|
|
7458
|
+
radii: shapeData.radii,
|
|
7459
|
+
labelIds,
|
|
7460
|
+
orientations
|
|
7461
|
+
};
|
|
7462
|
+
}
|
|
6071
7463
|
/**
|
|
6072
|
-
*
|
|
6073
|
-
*
|
|
6074
|
-
* few hundred device pixels) is an extreme, unusual upscale factor that has
|
|
6075
|
-
* been observed to trigger a Chromium GPU-rasterization glitch — a `fill:
|
|
6076
|
-
* none` shape painting as solid-filled, matching its computed style exactly
|
|
6077
|
-
* but not what's on screen. Rendering in a "normal"-magnitude SVG coordinate
|
|
6078
|
-
* system (viewBox radius {@link RENDER_SCALE} instead of `1`) avoids it.
|
|
7464
|
+
* Paint order: far-from-focus first so nearer (larger) nodes sit on top;
|
|
7465
|
+
* then labelled over unlabelled; the hovered node above everything.
|
|
6079
7466
|
*/
|
|
6080
|
-
|
|
7467
|
+
function paintOrder(nodes, ctx, isTickled) {
|
|
7468
|
+
const layer = (n) => isTickled(n.id) ? 2 : ctx.labelIds.has(n.id) ? 1 : 0;
|
|
7469
|
+
const distance = (n) => n.position.x ** 2 + n.position.y ** 2;
|
|
7470
|
+
return [...nodes].sort((a, b) => layer(a) - layer(b) || distance(b) - distance(a));
|
|
7471
|
+
}
|
|
7472
|
+
//#endregion
|
|
7473
|
+
//#region src/hyperbolic-tree/render-templates.ts
|
|
6081
7474
|
/**
|
|
6082
7475
|
* Sanitizes a node key into a token safe to use inside a `part` attribute /
|
|
6083
7476
|
* `::part()` selector — arbitrary key characters (spaces, quotes, etc.)
|
|
@@ -6086,13 +7479,121 @@ const RENDER_SCALE = 500;
|
|
|
6086
7479
|
function partToken(key) {
|
|
6087
7480
|
return key.replace(/[^\w-]/g, `-`);
|
|
6088
7481
|
}
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
7482
|
+
const preventMouseDownFocus = (e) => e.preventDefault();
|
|
7483
|
+
/** The shared `<g part="node">` wrapper — ARIA, parts, weight/branch vars, tooltip, and pointer handling. */
|
|
7484
|
+
function renderNodeGroup(n, args, transform, content) {
|
|
7485
|
+
const { node, key, id } = n;
|
|
7486
|
+
const { ctx, state, handlers } = args;
|
|
7487
|
+
const selected = state.isSelected(node);
|
|
7488
|
+
const classes = [
|
|
7489
|
+
`node`,
|
|
7490
|
+
selected ? `selected` : ``,
|
|
7491
|
+
state.isTickled(id) ? `tickled` : ``,
|
|
7492
|
+
state.isFiltered(n) ? `filtered` : ``,
|
|
7493
|
+
state.isPending(node) ? `loading` : ``,
|
|
7494
|
+
n.ring === 0 ? `focused` : ``
|
|
7495
|
+
].filter(Boolean).join(` `);
|
|
7496
|
+
const siblings = ctx.siblingGroups.get(n.parentId) ?? [n];
|
|
7497
|
+
const weightStyle = `--node-weight: ${n.weightNormalized};${n.weight === void 0 ? `` : ` --node-weight-raw: ${n.weight};`}`;
|
|
7498
|
+
return svg`
|
|
7499
|
+
<g
|
|
7500
|
+
class=${classes}
|
|
7501
|
+
data-key=${key}
|
|
7502
|
+
role="treeitem"
|
|
7503
|
+
tabindex="-1"
|
|
7504
|
+
aria-level=${n.depth + 1}
|
|
7505
|
+
aria-selected=${selected}
|
|
7506
|
+
aria-setsize=${siblings.length}
|
|
7507
|
+
aria-posinset=${siblings.findIndex((sib) => sib.id === id) + 1}
|
|
7508
|
+
aria-label=${node.item.label}
|
|
7509
|
+
part="node node-${partToken(key)}"
|
|
7510
|
+
style=${weightStyle + branchStyle(id, ctx.branches)}
|
|
7511
|
+
transform=${transform}
|
|
7512
|
+
@pointerdown=${handlers.onPointerDown}
|
|
7513
|
+
@mousedown=${preventMouseDownFocus}
|
|
7514
|
+
@click=${(e) => handlers.onClick(n, e)}
|
|
7515
|
+
@dblclick=${(e) => handlers.onDblClick(n, e)}
|
|
7516
|
+
@pointerenter=${() => handlers.onPointerEnter(id)}
|
|
7517
|
+
>${state.showTitle(n) ? svg`<title>${node.item.label}</title>` : nothing}${content}</g>
|
|
7518
|
+
`;
|
|
7519
|
+
}
|
|
7520
|
+
const degrees = (radians) => radians * 180 / Math.PI;
|
|
7521
|
+
/** A dot's label: just past the dot, running along its orientation — flipped labels anchor at their end so they still read outward. */
|
|
7522
|
+
function renderDotLabel(n, ctx, scaledRadius) {
|
|
7523
|
+
const { textAngle, flipped } = ctx.orientations.get(n.id);
|
|
7524
|
+
const gap = scaledRadius + 5;
|
|
7525
|
+
return svg`<text
|
|
7526
|
+
class="hyperbolictree-label"
|
|
7527
|
+
part="label"
|
|
7528
|
+
x=${flipped ? -gap : gap}
|
|
7529
|
+
y="0"
|
|
7530
|
+
transform=${textAngle === 0 ? nothing : `rotate(${degrees(textAngle)})`}
|
|
7531
|
+
text-anchor=${flipped ? `end` : `start`}
|
|
7532
|
+
font-size=${ctx.font.units}
|
|
7533
|
+
dominant-baseline="middle"
|
|
7534
|
+
>${n.node.item.label}</text>`;
|
|
7535
|
+
}
|
|
7536
|
+
function renderDotNode(n, args) {
|
|
7537
|
+
const { ctx } = args;
|
|
7538
|
+
const scaledRadius = ctx.radii.get(n.id) * 500;
|
|
7539
|
+
return renderNodeGroup(n, args, `translate(${n.position.x * 500} ${n.position.y * 500})`, svg`
|
|
7540
|
+
<circle class="node-hit" r=${Math.max(scaledRadius, 17.5)} part="hit" />
|
|
7541
|
+
<circle class="node-dot" r=${scaledRadius} part="dot" />
|
|
7542
|
+
${ctx.labelIds.has(n.id) ? renderDotLabel(n, ctx, scaledRadius) : nothing}
|
|
7543
|
+
`);
|
|
7544
|
+
}
|
|
7545
|
+
function renderBoxNode(n, args) {
|
|
7546
|
+
const { ctx } = args;
|
|
7547
|
+
const box = ctx.boxes.get(n.id);
|
|
7548
|
+
const showText = ctx.labelIds.has(n.id);
|
|
7549
|
+
const width = showText ? box.width : box.markerSize;
|
|
7550
|
+
const height = showText ? box.height : box.markerSize;
|
|
7551
|
+
const { textAngle } = ctx.orientations.get(n.id);
|
|
7552
|
+
const rotate = showText && textAngle !== 0 ? ` rotate(${degrees(textAngle)})` : ``;
|
|
7553
|
+
return renderNodeGroup(n, args, `translate(${n.position.x * 500} ${n.position.y * 500})${rotate} scale(${box.scale})`, svg`
|
|
7554
|
+
<rect
|
|
7555
|
+
class="node-box ${showText ? `` : `marker`}"
|
|
7556
|
+
part="box"
|
|
7557
|
+
x=${-width / 2}
|
|
7558
|
+
y=${-height / 2}
|
|
7559
|
+
width=${width}
|
|
7560
|
+
height=${height}
|
|
7561
|
+
rx=${ctx.font.units * .2}
|
|
7562
|
+
/>
|
|
7563
|
+
${showText ? svg`<text class="hyperbolictree-label box-label" part="label" x="0" y="0" font-size=${ctx.font.units} text-anchor="middle" dominant-baseline="central">${box.text}</text>` : nothing}
|
|
7564
|
+
`);
|
|
7565
|
+
}
|
|
7566
|
+
/** Every visible node, in paint order, in the shape `ctx` was built for. */
|
|
7567
|
+
function renderNodes(nodes, shape, args) {
|
|
7568
|
+
const ordered = paintOrder(nodes, args.ctx, args.state.isTickled);
|
|
7569
|
+
const renderOne = shape === `box` ? renderBoxNode : renderDotNode;
|
|
7570
|
+
return repeat(ordered, (n) => n.id, (n) => renderOne(n, args));
|
|
7571
|
+
}
|
|
7572
|
+
function renderDefaultEdge(edge, branches) {
|
|
7573
|
+
return svg`<path
|
|
7574
|
+
class="edge"
|
|
7575
|
+
d=${geodesicPath(edge.from, edge.to, 500)}
|
|
7576
|
+
part="edge edge-to-${partToken(edge.toKey)} edge-from-${partToken(edge.fromKey)}"
|
|
7577
|
+
data-from=${edge.fromKey}
|
|
7578
|
+
data-to=${edge.toKey}
|
|
7579
|
+
style="--edge-weight: ${edge.weightNormalized};${branchStyle(edge.toId, branches)}"
|
|
7580
|
+
/>`;
|
|
7581
|
+
}
|
|
7582
|
+
/** Every visible edge — via `edgeRenderer` when given, else the default geodesic arc. */
|
|
7583
|
+
function renderEdges(edges, byId, branches, edgeRenderer) {
|
|
7584
|
+
return repeat(edges, (e) => `${e.fromId}->${e.toId}`, (edge) => {
|
|
7585
|
+
const fromNode = byId.get(edge.fromId)?.node;
|
|
7586
|
+
const toNode = byId.get(edge.toId)?.node;
|
|
7587
|
+
if (edgeRenderer && fromNode && toNode) return svg`<g transform="scale(${500})">${edgeRenderer({
|
|
7588
|
+
edge,
|
|
7589
|
+
fromNode,
|
|
7590
|
+
toNode
|
|
7591
|
+
})}</g>`;
|
|
7592
|
+
return renderDefaultEdge(edge, branches);
|
|
7593
|
+
});
|
|
6095
7594
|
}
|
|
7595
|
+
//#endregion
|
|
7596
|
+
//#region src/hyperbolic-tree/hyperbolictree-list.ts
|
|
6096
7597
|
let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitElement {
|
|
6097
7598
|
constructor(..._args) {
|
|
6098
7599
|
super(..._args);
|
|
@@ -6100,37 +7601,63 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
6100
7601
|
this.selectionFilter = `all`;
|
|
6101
7602
|
this.interactionMode = `standard`;
|
|
6102
7603
|
this.maxRadius = 4;
|
|
7604
|
+
this.nodeShape = `dot`;
|
|
7605
|
+
this.boxMaxChars = 24;
|
|
7606
|
+
this.minLabelSize = 9;
|
|
7607
|
+
this.colorByBranch = false;
|
|
6103
7608
|
this.nodeRadius = .05;
|
|
6104
7609
|
this.minNodeRadius = .015;
|
|
6105
7610
|
this.minWeightScale = .6;
|
|
6106
7611
|
this.maxWeightScale = 1.6;
|
|
6107
7612
|
this.zoom = 1;
|
|
7613
|
+
this.disableZoomGestures = false;
|
|
7614
|
+
this.disableRadialLabels = false;
|
|
7615
|
+
this.disableChildPreview = false;
|
|
6108
7616
|
this.curvature = 1;
|
|
7617
|
+
this.siblingSpacing = DEFAULT_SIBLING_SPACING;
|
|
6109
7618
|
this.focusAnimationDuration = 400;
|
|
6110
7619
|
this._selectedNodes = /* @__PURE__ */ new Set();
|
|
6111
7620
|
this.disableKeyboardNavigation = false;
|
|
6112
7621
|
this.registry = void 0;
|
|
6113
|
-
this.
|
|
6114
|
-
this.
|
|
6115
|
-
this.
|
|
7622
|
+
this._focusId = void 0;
|
|
7623
|
+
this._viewOverride = void 0;
|
|
7624
|
+
this._focusRotation = 0;
|
|
7625
|
+
this._zoomOffset = {
|
|
6116
7626
|
x: 0,
|
|
6117
7627
|
y: 0
|
|
6118
7628
|
};
|
|
7629
|
+
this._previewId = void 0;
|
|
7630
|
+
this.#preview = createChildPreviewTiming({
|
|
7631
|
+
candidate: (id) => {
|
|
7632
|
+
const nodes = this.#layout().nodes;
|
|
7633
|
+
const n = id === void 0 || this.disableChildPreview ? void 0 : nodes.find((v) => v.id === id);
|
|
7634
|
+
return n && needsChildPreview(n, nodes, this.#lastLabelIds) ? n.id : void 0;
|
|
7635
|
+
},
|
|
7636
|
+
setPreviewId: (id) => {
|
|
7637
|
+
this._previewId = id;
|
|
7638
|
+
}
|
|
7639
|
+
});
|
|
7640
|
+
this.#lazy = createLazyChildLoader(() => ({
|
|
7641
|
+
model: this.model,
|
|
7642
|
+
loadChildren: this.loadChildren
|
|
7643
|
+
}), () => this.requestUpdate());
|
|
7644
|
+
this.#lastLabelIds = /* @__PURE__ */ new Set();
|
|
6119
7645
|
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
6120
7646
|
labelSelector: `.hyperbolictree-label`,
|
|
6121
7647
|
highlightKey: `incr-search-highlight`
|
|
6122
7648
|
}) });
|
|
6123
7649
|
this.tickled = new TickledController(this, {
|
|
6124
7650
|
getItems: () => this.#layout().nodes.map((n) => ({
|
|
6125
|
-
id: n.
|
|
7651
|
+
id: n.id,
|
|
6126
7652
|
enabled: true
|
|
6127
7653
|
})),
|
|
6128
7654
|
onStateChange: () => {
|
|
6129
7655
|
this.setAttribute(`data-input-mode`, this.tickled.inputMode);
|
|
6130
7656
|
},
|
|
6131
7657
|
onTickle: (id) => {
|
|
7658
|
+
this.#preview.update(id);
|
|
6132
7659
|
if (id === void 0) return;
|
|
6133
|
-
const n = this.#layout().nodes.find((n) => n.
|
|
7660
|
+
const n = this.#layout().nodes.find((n) => n.id === id);
|
|
6134
7661
|
if (!n) return;
|
|
6135
7662
|
this.dispatchEvent(new CustomEvent(`tickle`, {
|
|
6136
7663
|
detail: {
|
|
@@ -6142,8 +7669,30 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
6142
7669
|
}));
|
|
6143
7670
|
}
|
|
6144
7671
|
});
|
|
6145
|
-
this
|
|
6146
|
-
|
|
7672
|
+
this._hostSize = {
|
|
7673
|
+
width: 0,
|
|
7674
|
+
height: 0
|
|
7675
|
+
};
|
|
7676
|
+
this.#gestures = createGestureHandlers({
|
|
7677
|
+
svgRect: () => this.renderRoot.querySelector(`svg.hyperbolictree`)?.getBoundingClientRect(),
|
|
7678
|
+
takeView: () => {
|
|
7679
|
+
const view = this.#currentView();
|
|
7680
|
+
this.#cancelAnimation();
|
|
7681
|
+
this._viewOverride = view;
|
|
7682
|
+
return view;
|
|
7683
|
+
},
|
|
7684
|
+
setView: (view) => {
|
|
7685
|
+
this._viewOverride = view;
|
|
7686
|
+
},
|
|
7687
|
+
zoomState: () => this.#zoomState(),
|
|
7688
|
+
setZoomState: (state) => {
|
|
7689
|
+
this.zoom = state.zoom;
|
|
7690
|
+
this._zoomOffset = state.offset;
|
|
7691
|
+
},
|
|
7692
|
+
zoomGesturesEnabled: () => !this.disableZoomGestures,
|
|
7693
|
+
hasFocus: () => this.renderRoot.activeElement !== null,
|
|
7694
|
+
focusTree: () => this.#focusTree()
|
|
7695
|
+
});
|
|
6147
7696
|
}
|
|
6148
7697
|
static {
|
|
6149
7698
|
this.shadowRootOptions = {
|
|
@@ -6156,37 +7705,55 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
6156
7705
|
}
|
|
6157
7706
|
/** The node currently centered on the disk. Pure view-state — `root` is never mutated by focus changes. */
|
|
6158
7707
|
get focusedNode() {
|
|
6159
|
-
if (this.
|
|
6160
|
-
return
|
|
7708
|
+
if (this._focusId === void 0) return this.#effectiveRoot;
|
|
7709
|
+
return findHyperbolicPathById(this.#effectiveRoot, this._focusId)?.at(-1) ?? this.#effectiveRoot;
|
|
6161
7710
|
}
|
|
7711
|
+
#preview;
|
|
7712
|
+
#lazy;
|
|
7713
|
+
/** Labels shown in the last render — what the child preview judges legibility by. */
|
|
7714
|
+
#lastLabelIds;
|
|
6162
7715
|
#modelUnsubscribe;
|
|
6163
|
-
#pendingKeys;
|
|
6164
|
-
#aborters;
|
|
6165
7716
|
#ownedRegistry;
|
|
6166
7717
|
#unregisterSearchCommands;
|
|
6167
7718
|
#animFrame;
|
|
6168
|
-
#
|
|
7719
|
+
#resizeObserver;
|
|
7720
|
+
#gestures;
|
|
6169
7721
|
get #effectiveRoot() {
|
|
6170
7722
|
return this.model?.root ?? this.root;
|
|
6171
7723
|
}
|
|
6172
7724
|
#layout() {
|
|
6173
7725
|
return computeHyperbolicLayout(this.#effectiveRoot, {
|
|
6174
|
-
focusedKey: this.
|
|
7726
|
+
focusedKey: this._focusId,
|
|
6175
7727
|
maxRadius: this.maxRadius,
|
|
6176
7728
|
curvature: this.curvature,
|
|
6177
|
-
|
|
7729
|
+
siblingSpacing: this.siblingSpacing,
|
|
7730
|
+
view: this.#currentView()
|
|
6178
7731
|
});
|
|
6179
7732
|
}
|
|
6180
|
-
#
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
7733
|
+
get #rootId() {
|
|
7734
|
+
return this.#effectiveRoot?.item.key;
|
|
7735
|
+
}
|
|
7736
|
+
/** The projection currently on screen, from the focus's local frame. */
|
|
7737
|
+
#currentView() {
|
|
7738
|
+
return this._viewOverride ?? rotationIsometry(this._focusRotation);
|
|
7739
|
+
}
|
|
7740
|
+
/**
|
|
7741
|
+
* The current view re-expressed in `targetId`'s local frame — i.e. the same
|
|
7742
|
+
* picture, but with `targetId` as the reference node. Where the view hands
|
|
7743
|
+
* over on a focus change.
|
|
7744
|
+
*/
|
|
7745
|
+
#viewFromFrameOf(targetId, currentId) {
|
|
7746
|
+
const entries = computeCanonicalPositions(this.#effectiveRoot, 24, this.curvature, this.siblingSpacing);
|
|
7747
|
+
const relative = currentId === void 0 ? void 0 : hyperbolicRelativeFrame(entries, currentId, targetId);
|
|
7748
|
+
return relative ? composeIsometry(this.#currentView(), relative) : rotationIsometry(this._focusRotation);
|
|
7749
|
+
}
|
|
7750
|
+
/** Layout id of `node` — by identity, falling back to the first node sharing its key. */
|
|
7751
|
+
#idOf(node) {
|
|
7752
|
+
const root = this.#effectiveRoot;
|
|
7753
|
+
const direct = findHyperbolicIdOf(root, node);
|
|
7754
|
+
if (direct !== void 0) return direct;
|
|
7755
|
+
const byKey = findHyperbolicNode(root, node.item.key);
|
|
7756
|
+
return byKey ? findHyperbolicIdOf(root, byKey) : void 0;
|
|
6190
7757
|
}
|
|
6191
7758
|
connectedCallback() {
|
|
6192
7759
|
super.connectedCallback();
|
|
@@ -6206,14 +7773,25 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
6206
7773
|
});
|
|
6207
7774
|
this.registry = registry;
|
|
6208
7775
|
this.#syncSearchCommands();
|
|
7776
|
+
if (typeof ResizeObserver === `function`) {
|
|
7777
|
+
this.#resizeObserver = new ResizeObserver(([entry]) => {
|
|
7778
|
+
const { width, height } = entry.contentRect;
|
|
7779
|
+
this._hostSize = {
|
|
7780
|
+
width,
|
|
7781
|
+
height
|
|
7782
|
+
};
|
|
7783
|
+
});
|
|
7784
|
+
this.#resizeObserver.observe(this);
|
|
7785
|
+
}
|
|
6209
7786
|
}
|
|
6210
7787
|
disconnectedCallback() {
|
|
6211
7788
|
super.disconnectedCallback();
|
|
7789
|
+
this.#resizeObserver?.disconnect();
|
|
7790
|
+
this.#resizeObserver = void 0;
|
|
6212
7791
|
this.#modelUnsubscribe?.();
|
|
6213
7792
|
this.#modelUnsubscribe = void 0;
|
|
6214
|
-
|
|
6215
|
-
this.#
|
|
6216
|
-
this.#pendingKeys.clear();
|
|
7793
|
+
this.#preview.dispose();
|
|
7794
|
+
this.#lazy.abortAll();
|
|
6217
7795
|
if (this.#animFrame !== void 0) {
|
|
6218
7796
|
cancelAnimationFrame(this.#animFrame);
|
|
6219
7797
|
this.#animFrame = void 0;
|
|
@@ -6254,58 +7832,30 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
6254
7832
|
}
|
|
6255
7833
|
if (changedProps.has(`interactionMode`)) this.clearSelection();
|
|
6256
7834
|
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
6257
|
-
if (this.
|
|
6258
|
-
this.
|
|
6259
|
-
this.
|
|
7835
|
+
if (this._focusId !== void 0 && !findHyperbolicPathById(this.#effectiveRoot, this._focusId)) {
|
|
7836
|
+
this._focusId = void 0;
|
|
7837
|
+
this._viewOverride = void 0;
|
|
7838
|
+
this._focusRotation = 0;
|
|
6260
7839
|
}
|
|
6261
|
-
this.#
|
|
7840
|
+
this.#lazy.ensure(this.#layout().nodes);
|
|
6262
7841
|
}
|
|
6263
7842
|
#resetViewState() {
|
|
6264
7843
|
this.#clearSelectionInternal();
|
|
6265
7844
|
this.tickled.clearTickled();
|
|
6266
|
-
this.
|
|
6267
|
-
this.
|
|
6268
|
-
this.
|
|
7845
|
+
this._focusId = void 0;
|
|
7846
|
+
this._viewOverride = void 0;
|
|
7847
|
+
this._focusRotation = 0;
|
|
7848
|
+
this._zoomOffset = {
|
|
6269
7849
|
x: 0,
|
|
6270
7850
|
y: 0
|
|
6271
7851
|
};
|
|
7852
|
+
this._previewId = void 0;
|
|
7853
|
+
this.#gestures.cancel();
|
|
6272
7854
|
if (this.#animFrame !== void 0) {
|
|
6273
7855
|
cancelAnimationFrame(this.#animFrame);
|
|
6274
7856
|
this.#animFrame = void 0;
|
|
6275
7857
|
}
|
|
6276
7858
|
}
|
|
6277
|
-
#ensureVisibleChildren() {
|
|
6278
|
-
if (!this.model && !this.loadChildren) return;
|
|
6279
|
-
for (const n of this.#layout().nodes) {
|
|
6280
|
-
const { node } = n;
|
|
6281
|
-
if (node.children !== void 0 || this.#pendingKeys.has(node.item.key)) continue;
|
|
6282
|
-
this.#loadChildren(node, n.depth);
|
|
6283
|
-
}
|
|
6284
|
-
}
|
|
6285
|
-
async #loadChildren(node, depth) {
|
|
6286
|
-
const key = node.item.key;
|
|
6287
|
-
this.#pendingKeys.add(key);
|
|
6288
|
-
const controller = new AbortController();
|
|
6289
|
-
this.#aborters.set(key, controller);
|
|
6290
|
-
this.requestUpdate();
|
|
6291
|
-
try {
|
|
6292
|
-
let children;
|
|
6293
|
-
if (this.model) children = await this.model.getChildren(node, depth, controller.signal);
|
|
6294
|
-
else children = [...await collectDataResult(this.loadChildren({
|
|
6295
|
-
node,
|
|
6296
|
-
depth
|
|
6297
|
-
}, controller.signal), controller.signal)];
|
|
6298
|
-
if (controller.signal.aborted) return;
|
|
6299
|
-
node.children = children;
|
|
6300
|
-
} catch {
|
|
6301
|
-
if (controller.signal.aborted) return;
|
|
6302
|
-
node.children = [];
|
|
6303
|
-
} finally {
|
|
6304
|
-
this.#pendingKeys.delete(key);
|
|
6305
|
-
this.#aborters.delete(key);
|
|
6306
|
-
if (!controller.signal.aborted) this.requestUpdate();
|
|
6307
|
-
}
|
|
6308
|
-
}
|
|
6309
7859
|
#canSelect(node) {
|
|
6310
7860
|
if (this.selectionFilter === `none`) return false;
|
|
6311
7861
|
if (this.selectionFilter === `leaf`) return node.item.isLeaf === true;
|
|
@@ -6381,83 +7931,83 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
6381
7931
|
* the previous one, for interop with generic `TreeComponent` listeners.
|
|
6382
7932
|
*/
|
|
6383
7933
|
focusOn(node) {
|
|
6384
|
-
const
|
|
6385
|
-
const
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
const
|
|
6389
|
-
const
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
y: 0
|
|
6396
|
-
};
|
|
6397
|
-
this._focusKey = targetKey;
|
|
6398
|
-
this._panOffset = {
|
|
6399
|
-
x: 0,
|
|
6400
|
-
y: 0
|
|
6401
|
-
};
|
|
6402
|
-
this.#animateTo(fromPosition, toPosition);
|
|
7934
|
+
const root = this.#effectiveRoot;
|
|
7935
|
+
const targetId = this.#idOf(node);
|
|
7936
|
+
const currentId = this._focusId ?? this.#rootId;
|
|
7937
|
+
if (targetId === void 0 || currentId === targetId) return;
|
|
7938
|
+
const targetNode = findHyperbolicPathById(root, targetId)?.at(-1) ?? node;
|
|
7939
|
+
const previousNode = currentId === void 0 ? void 0 : findHyperbolicPathById(root, currentId)?.at(-1);
|
|
7940
|
+
const fromView = this.#viewFromFrameOf(targetId, currentId);
|
|
7941
|
+
this._focusId = targetId;
|
|
7942
|
+
this.#gestures.cancel();
|
|
7943
|
+
this._previewId = void 0;
|
|
7944
|
+
this.#animateTo(fromView);
|
|
6403
7945
|
this.dispatchEvent(new CustomEvent(`focus`, {
|
|
6404
7946
|
detail: {
|
|
6405
|
-
node,
|
|
7947
|
+
node: targetNode,
|
|
6406
7948
|
previous: previousNode
|
|
6407
7949
|
},
|
|
6408
7950
|
bubbles: true,
|
|
6409
7951
|
composed: true
|
|
6410
7952
|
}));
|
|
6411
|
-
if (previousNode) {
|
|
6412
|
-
if (
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
},
|
|
6429
|
-
bubbles: true,
|
|
6430
|
-
composed: true
|
|
6431
|
-
}));
|
|
6432
|
-
}
|
|
7953
|
+
if (previousNode && currentId !== void 0) {
|
|
7954
|
+
if (isHyperbolicDescendantId(targetId, currentId)) this.dispatchEvent(new CustomEvent(`expand`, {
|
|
7955
|
+
detail: {
|
|
7956
|
+
node: targetNode,
|
|
7957
|
+
depth: hyperbolicIdDepth(targetId)
|
|
7958
|
+
},
|
|
7959
|
+
bubbles: true,
|
|
7960
|
+
composed: true
|
|
7961
|
+
}));
|
|
7962
|
+
else if (isHyperbolicDescendantId(currentId, targetId)) this.dispatchEvent(new CustomEvent(`collapse`, {
|
|
7963
|
+
detail: {
|
|
7964
|
+
node: previousNode,
|
|
7965
|
+
depth: hyperbolicIdDepth(currentId)
|
|
7966
|
+
},
|
|
7967
|
+
bubbles: true,
|
|
7968
|
+
composed: true
|
|
7969
|
+
}));
|
|
6433
7970
|
}
|
|
6434
7971
|
this.requestUpdate();
|
|
6435
7972
|
}
|
|
6436
|
-
#
|
|
6437
|
-
return typeof matchMedia === `function` && matchMedia(`(prefers-reduced-motion: reduce)`).matches;
|
|
6438
|
-
}
|
|
6439
|
-
#animateTo(from, to) {
|
|
7973
|
+
#cancelAnimation() {
|
|
6440
7974
|
if (this.#animFrame !== void 0) {
|
|
6441
7975
|
cancelAnimationFrame(this.#animFrame);
|
|
6442
7976
|
this.#animFrame = void 0;
|
|
6443
7977
|
}
|
|
6444
|
-
|
|
6445
|
-
|
|
7978
|
+
}
|
|
7979
|
+
/** Animate from view `from` (in the new focus's frame) to the settled, centered view — see {@link focusTransition}. */
|
|
7980
|
+
#animateTo(from) {
|
|
7981
|
+
this.#cancelAnimation();
|
|
7982
|
+
const { viewAt, settledRotation } = focusTransition(from);
|
|
7983
|
+
this._focusRotation = settledRotation;
|
|
7984
|
+
const fromOffset = this._zoomOffset;
|
|
7985
|
+
if (this.focusAnimationDuration <= 0 || prefersReducedMotion$1() || typeof requestAnimationFrame !== `function`) {
|
|
7986
|
+
this._viewOverride = void 0;
|
|
7987
|
+
this._zoomOffset = {
|
|
7988
|
+
x: 0,
|
|
7989
|
+
y: 0
|
|
7990
|
+
};
|
|
6446
7991
|
return;
|
|
6447
7992
|
}
|
|
6448
7993
|
const start = performance.now();
|
|
6449
7994
|
const duration = this.focusAnimationDuration;
|
|
6450
7995
|
const step = (now) => {
|
|
6451
7996
|
const t = Math.min(1, (now - start) / duration);
|
|
6452
|
-
|
|
6453
|
-
this.
|
|
7997
|
+
const remaining = 1 - easeInOutCubic(t);
|
|
7998
|
+
this._zoomOffset = {
|
|
7999
|
+
x: fromOffset.x * remaining,
|
|
8000
|
+
y: fromOffset.y * remaining
|
|
8001
|
+
};
|
|
8002
|
+
this._viewOverride = t >= 1 ? void 0 : viewAt(t);
|
|
6454
8003
|
this.#animFrame = t < 1 ? requestAnimationFrame(step) : void 0;
|
|
6455
8004
|
};
|
|
6456
8005
|
this.#animFrame = requestAnimationFrame(step);
|
|
6457
8006
|
}
|
|
6458
8007
|
#focusAndMaybeSelect(node) {
|
|
6459
8008
|
this.focusOn(node);
|
|
6460
|
-
this
|
|
8009
|
+
const id = this.#idOf(node);
|
|
8010
|
+
if (id !== void 0) this.tickled.setTickled(id);
|
|
6461
8011
|
if (this.interactionMode === `implicit`) this.select(node);
|
|
6462
8012
|
}
|
|
6463
8013
|
/** Focus + select the node at `keys` (path from the root's children). Returns `true` if found. */
|
|
@@ -6472,7 +8022,8 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
6472
8022
|
}
|
|
6473
8023
|
this.focusOn(current);
|
|
6474
8024
|
this.select(current);
|
|
6475
|
-
this
|
|
8025
|
+
const id = this.#idOf(current);
|
|
8026
|
+
if (id !== void 0) this.tickled.setTickled(id);
|
|
6476
8027
|
this.requestUpdate();
|
|
6477
8028
|
return true;
|
|
6478
8029
|
}
|
|
@@ -6480,7 +8031,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
6480
8031
|
navigateToNode(predicate) {
|
|
6481
8032
|
const root = this.#effectiveRoot;
|
|
6482
8033
|
if (!root?.children) return false;
|
|
6483
|
-
const keyPath =
|
|
8034
|
+
const keyPath = findHyperbolicKeyPath(root.children, predicate);
|
|
6484
8035
|
if (!keyPath) return false;
|
|
6485
8036
|
return this.navigateTo(keyPath);
|
|
6486
8037
|
}
|
|
@@ -6491,22 +8042,13 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
6491
8042
|
*/
|
|
6492
8043
|
revealKey(key) {
|
|
6493
8044
|
const node = findHyperbolicNode(this.#effectiveRoot, key);
|
|
6494
|
-
|
|
6495
|
-
if (!
|
|
6496
|
-
this.
|
|
8045
|
+
const id = node ? this.#idOf(node) : void 0;
|
|
8046
|
+
if (!node || id === void 0) return false;
|
|
8047
|
+
if (!this.#layout().nodes.some((n) => n.id === id)) this.focusOn(node);
|
|
8048
|
+
this.tickled.setTickled(id);
|
|
6497
8049
|
this.requestUpdate();
|
|
6498
8050
|
return true;
|
|
6499
8051
|
}
|
|
6500
|
-
#findKeyPath(nodes, predicate, prefix = []) {
|
|
6501
|
-
for (const node of nodes) {
|
|
6502
|
-
const path = [...prefix, node.item.key];
|
|
6503
|
-
if (predicate(node)) return path;
|
|
6504
|
-
if (node.children?.length) {
|
|
6505
|
-
const hit = this.#findKeyPath(node.children, predicate, path);
|
|
6506
|
-
if (hit) return hit;
|
|
6507
|
-
}
|
|
6508
|
-
}
|
|
6509
|
-
}
|
|
6510
8052
|
/**
|
|
6511
8053
|
* All nodes currently laid out on the disk (within `maxRadius` of the
|
|
6512
8054
|
* focus), in render (ring-major) order. Unlike `ixfx-icicletree-list`, the
|
|
@@ -6543,7 +8085,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
6543
8085
|
this.requestUpdate();
|
|
6544
8086
|
}
|
|
6545
8087
|
} else if (this.interactionMode !== `manual`) this.select(n.node);
|
|
6546
|
-
this.tickled.setTickled(n.
|
|
8088
|
+
this.tickled.setTickled(n.id);
|
|
6547
8089
|
this.focusOn(n.node);
|
|
6548
8090
|
}
|
|
6549
8091
|
#handleItemDblClick(n) {
|
|
@@ -6565,239 +8107,147 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
6565
8107
|
}));
|
|
6566
8108
|
}
|
|
6567
8109
|
#handleKeyDown(event) {
|
|
6568
|
-
if (this.disableKeyboardNavigation) return;
|
|
6569
8110
|
const root = this.#effectiveRoot;
|
|
6570
|
-
|
|
6571
|
-
if (
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
}
|
|
6590
|
-
case `ArrowLeft`:
|
|
6591
|
-
case `ArrowRight`: {
|
|
6592
|
-
event.preventDefault();
|
|
6593
|
-
const path = findHyperbolicPath(root, focusNode.item.key);
|
|
6594
|
-
const siblings = (path && path.length >= 2 ? path[path.length - 2] : void 0)?.children ?? (focusNode === root ? [focusNode] : []);
|
|
6595
|
-
const index = siblings.findIndex((n) => n.item.key === focusNode.item.key);
|
|
6596
|
-
if (index === -1 || siblings.length <= 1) return;
|
|
6597
|
-
const next = siblings[(index + (event.key === `ArrowRight` ? 1 : -1) + siblings.length) % siblings.length];
|
|
6598
|
-
this.#focusAndMaybeSelect(next);
|
|
6599
|
-
return;
|
|
6600
|
-
}
|
|
6601
|
-
case `Enter`: {
|
|
6602
|
-
event.preventDefault();
|
|
6603
|
-
if (this.interactionMode !== `manual`) this.select(focusNode);
|
|
6604
|
-
const depth = (findHyperbolicPath(root, focusNode.item.key)?.length ?? 1) - 1;
|
|
6605
|
-
this.dispatchEvent(new CustomEvent(`activate`, {
|
|
6606
|
-
detail: {
|
|
6607
|
-
node: focusNode,
|
|
6608
|
-
depth
|
|
6609
|
-
},
|
|
6610
|
-
bubbles: true,
|
|
6611
|
-
composed: true
|
|
6612
|
-
}));
|
|
6613
|
-
return;
|
|
6614
|
-
}
|
|
6615
|
-
case `Escape`: if (this._focusKey !== void 0 && this._focusKey !== root.item.key) {
|
|
6616
|
-
event.preventDefault();
|
|
6617
|
-
this.focusOn(root);
|
|
6618
|
-
}
|
|
8111
|
+
if (this.disableKeyboardNavigation || !root) return;
|
|
8112
|
+
if (event.key === `Escape` && this._previewId !== void 0) {
|
|
8113
|
+
event.preventDefault();
|
|
8114
|
+
this._previewId = void 0;
|
|
8115
|
+
return;
|
|
8116
|
+
}
|
|
8117
|
+
const { action, handled } = resolveKeyboardAction(event.key, root, this._focusId ?? root.item.key, this.#layout().nodes);
|
|
8118
|
+
if (handled) event.preventDefault();
|
|
8119
|
+
if (action.kind === `focus`) this.#focusAndMaybeSelect(action.node);
|
|
8120
|
+
else if (action.kind === `activate`) {
|
|
8121
|
+
if (this.interactionMode !== `manual`) this.select(action.node);
|
|
8122
|
+
this.dispatchEvent(new CustomEvent(`activate`, {
|
|
8123
|
+
detail: {
|
|
8124
|
+
node: action.node,
|
|
8125
|
+
depth: action.depth
|
|
8126
|
+
},
|
|
8127
|
+
bubbles: true,
|
|
8128
|
+
composed: true
|
|
8129
|
+
}));
|
|
6619
8130
|
}
|
|
6620
8131
|
}
|
|
6621
|
-
#
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
if (!this.#isBackgroundTarget(event)) return;
|
|
6627
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
6628
|
-
this.#panStart = {
|
|
6629
|
-
pointerId: event.pointerId,
|
|
6630
|
-
startClient: {
|
|
6631
|
-
x: event.clientX,
|
|
6632
|
-
y: event.clientY
|
|
6633
|
-
},
|
|
6634
|
-
baseOffset: this._panOffset
|
|
6635
|
-
};
|
|
6636
|
-
}
|
|
6637
|
-
#handleBackgroundPointerMove(event) {
|
|
6638
|
-
if (!this.#panStart || event.pointerId !== this.#panStart.pointerId) return;
|
|
6639
|
-
const rect = event.currentTarget.getBoundingClientRect();
|
|
6640
|
-
const scale = 2.1 / (Math.min(rect.width, rect.height) || 1);
|
|
6641
|
-
const dx = (event.clientX - this.#panStart.startClient.x) * scale;
|
|
6642
|
-
const dy = (event.clientY - this.#panStart.startClient.y) * scale;
|
|
6643
|
-
this._panOffset = clampToDisk({
|
|
6644
|
-
x: this.#panStart.baseOffset.x - dx,
|
|
6645
|
-
y: this.#panStart.baseOffset.y - dy
|
|
6646
|
-
}, .9);
|
|
6647
|
-
this.requestUpdate();
|
|
6648
|
-
}
|
|
6649
|
-
#handleBackgroundPointerUp(event) {
|
|
6650
|
-
if (this.#panStart?.pointerId === event.pointerId) this.#panStart = void 0;
|
|
6651
|
-
}
|
|
6652
|
-
#defaultEdgeRenderer(edge) {
|
|
6653
|
-
const steps = 8;
|
|
6654
|
-
let d = ``;
|
|
6655
|
-
for (let i = 0; i <= steps; i++) {
|
|
6656
|
-
const p = geodesicInterpolate(edge.from, edge.to, i / steps);
|
|
6657
|
-
d += `${i === 0 ? `M` : `L`}${(p.x * RENDER_SCALE).toFixed(2)},${(p.y * RENDER_SCALE).toFixed(2)} `;
|
|
6658
|
-
}
|
|
6659
|
-
return svg`<path
|
|
6660
|
-
class="edge"
|
|
6661
|
-
d=${d}
|
|
6662
|
-
part="edge edge-to-${partToken(edge.toKey)} edge-from-${partToken(edge.fromKey)}"
|
|
6663
|
-
data-from=${edge.fromKey}
|
|
6664
|
-
data-to=${edge.toKey}
|
|
6665
|
-
style="--edge-weight: ${edge.weightNormalized}"
|
|
6666
|
-
/>`;
|
|
6667
|
-
}
|
|
6668
|
-
#renderEdge(edge, byKey) {
|
|
6669
|
-
if (this.edgeRenderer) {
|
|
6670
|
-
const fromNode = byKey.get(edge.fromKey)?.node;
|
|
6671
|
-
const toNode = byKey.get(edge.toKey)?.node;
|
|
6672
|
-
if (fromNode && toNode) return svg`<g transform="scale(${RENDER_SCALE})">${this.edgeRenderer({
|
|
6673
|
-
edge,
|
|
6674
|
-
fromNode,
|
|
6675
|
-
toNode
|
|
6676
|
-
})}</g>`;
|
|
6677
|
-
}
|
|
6678
|
-
return this.#defaultEdgeRenderer(edge);
|
|
8132
|
+
#zoomState() {
|
|
8133
|
+
return clampZoomState({
|
|
8134
|
+
zoom: this.zoom,
|
|
8135
|
+
offset: this._zoomOffset
|
|
8136
|
+
});
|
|
6679
8137
|
}
|
|
6680
8138
|
/**
|
|
6681
|
-
*
|
|
6682
|
-
*
|
|
6683
|
-
*
|
|
6684
|
-
*
|
|
6685
|
-
* bushy ancestor elsewhere in the tree, or many siblings at this level)
|
|
6686
|
-
* all render at the same distance-only size and physically overlap into
|
|
6687
|
-
* an unreadable blob, however small {@link minNodeRadius} suppresses their
|
|
6688
|
-
* labels. `angularShare * distance` approximates the Euclidean arc length
|
|
6689
|
-
* available to the node; capping the diameter to a fraction of that keeps
|
|
6690
|
-
* a visible gap between crowded neighbours.
|
|
8139
|
+
* Any pointer interaction (a node press, or a pan) moves keyboard focus to
|
|
8140
|
+
* the tree, so the user can carry straight on with the arrow keys.
|
|
8141
|
+
* Focuses the stable `<svg role="tree">` — not the node, which paint-order
|
|
8142
|
+
* re-sorting may move in the DOM (dropping its focus) — without scrolling.
|
|
6691
8143
|
*/
|
|
6692
|
-
#
|
|
6693
|
-
|
|
6694
|
-
const scale = Math.max(.2, 1 - distance);
|
|
6695
|
-
const weightScale = this.minWeightScale + (this.maxWeightScale - this.minWeightScale) * n.weightNormalized;
|
|
6696
|
-
const radius = this.nodeRadius * scale * weightScale;
|
|
6697
|
-
if (distance < 1e-6) return radius;
|
|
6698
|
-
const arcLength = n.angularShare * distance;
|
|
6699
|
-
return Math.min(radius, Math.max(arcLength * .35, .003));
|
|
8144
|
+
#focusTree() {
|
|
8145
|
+
this.renderRoot.querySelector(`svg.hyperbolictree`)?.focus({ preventScroll: true });
|
|
6700
8146
|
}
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
8147
|
+
#handleNodePointerDown(event) {
|
|
8148
|
+
event.stopPropagation();
|
|
8149
|
+
this.#focusTree();
|
|
8150
|
+
}
|
|
8151
|
+
#pickPreviewChild(child) {
|
|
8152
|
+
this.#preview.hide();
|
|
8153
|
+
if (this.interactionMode !== `manual`) this.select(child);
|
|
8154
|
+
const id = this.#idOf(child);
|
|
8155
|
+
if (id !== void 0) this.tickled.setTickled(id);
|
|
8156
|
+
this.focusOn(child);
|
|
8157
|
+
this.#focusTree();
|
|
8158
|
+
}
|
|
8159
|
+
#previewFor(nodes) {
|
|
8160
|
+
const n = this._previewId === void 0 || this.disableChildPreview ? void 0 : nodes.find((v) => v.id === this._previewId);
|
|
8161
|
+
if (!n) return void 0;
|
|
8162
|
+
const { width, height } = this._hostSize;
|
|
8163
|
+
const anchor = diskToHostPx(n.position, this.#zoomState(), width || 420, height || 420);
|
|
8164
|
+
return {
|
|
8165
|
+
node: n.node,
|
|
8166
|
+
anchor,
|
|
8167
|
+
above: anchor.y > (height || 420) / 2
|
|
8168
|
+
};
|
|
8169
|
+
}
|
|
8170
|
+
#nodeContext(nodes, branches) {
|
|
8171
|
+
return buildNodeRenderContext(nodes, {
|
|
8172
|
+
nodeShape: this.nodeShape,
|
|
8173
|
+
radialLabels: !this.disableRadialLabels,
|
|
8174
|
+
zoom: this.#zoomState().zoom,
|
|
8175
|
+
font: fontMetrics(this, Math.min(this._hostSize.width, this._hostSize.height), this.zoom),
|
|
8176
|
+
branches,
|
|
8177
|
+
boxMaxChars: this.boxMaxChars,
|
|
8178
|
+
minLabelSize: this.minLabelSize,
|
|
8179
|
+
nodeRadius: this.nodeRadius,
|
|
8180
|
+
minNodeRadius: this.minNodeRadius,
|
|
8181
|
+
minWeightScale: this.minWeightScale,
|
|
8182
|
+
maxWeightScale: this.maxWeightScale,
|
|
8183
|
+
isSelected: (node) => this._selectedNodes.has(node),
|
|
8184
|
+
isTickled: (id) => this.tickled.isTickled(id)
|
|
6714
8185
|
});
|
|
6715
|
-
const kept = [];
|
|
6716
|
-
const keys = /* @__PURE__ */ new Set();
|
|
6717
|
-
for (const n of prioritised) if (!kept.some((k) => Math.hypot(k.position.x - n.position.x, k.position.y - n.position.y) < minSpacing)) {
|
|
6718
|
-
kept.push(n);
|
|
6719
|
-
keys.add(n.key);
|
|
6720
|
-
}
|
|
6721
|
-
return keys;
|
|
6722
8186
|
}
|
|
6723
|
-
#
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
aria-label=${node.item.label}
|
|
6747
|
-
part="node node-${partToken(key)}"
|
|
6748
|
-
style=${weightStyle}
|
|
6749
|
-
transform="translate(${position.x * RENDER_SCALE} ${position.y * RENDER_SCALE})"
|
|
6750
|
-
@pointerdown=${(e) => e.stopPropagation()}
|
|
6751
|
-
@click=${(e) => {
|
|
6752
|
-
e.stopPropagation();
|
|
6753
|
-
this.#handleItemClick(n, e);
|
|
6754
|
-
}}
|
|
6755
|
-
@dblclick=${(e) => {
|
|
6756
|
-
e.stopPropagation();
|
|
6757
|
-
this.#handleItemDblClick(n);
|
|
6758
|
-
}}
|
|
6759
|
-
@pointerenter=${() => this.tickled.handlePointerEnter(key)}
|
|
6760
|
-
>
|
|
6761
|
-
<circle class="node-hit" r=${Math.max(scaledRadius, .035 * RENDER_SCALE)} part="hit" />
|
|
6762
|
-
<circle class="node-dot" r=${scaledRadius} part="dot" />
|
|
6763
|
-
${showLabel ? svg`<text class="hyperbolictree-label" part="label" x=${scaledRadius + .01 * RENDER_SCALE} y="0" dominant-baseline="middle">${node.item.label}</text>` : nothing}
|
|
6764
|
-
</g>
|
|
6765
|
-
`;
|
|
8187
|
+
#renderNodes(nodes, ctx) {
|
|
8188
|
+
return renderNodes(nodes, this.nodeShape, {
|
|
8189
|
+
ctx,
|
|
8190
|
+
state: {
|
|
8191
|
+
isSelected: (node) => this._selectedNodes.has(node),
|
|
8192
|
+
isTickled: (id) => this.tickled.isTickled(id),
|
|
8193
|
+
showTitle: (n) => this.disableChildPreview || !n.node.children?.length,
|
|
8194
|
+
isFiltered: (n) => !this.#matchesFilter(n.node, n.depth),
|
|
8195
|
+
isPending: (node) => this.#lazy.isPending(node)
|
|
8196
|
+
},
|
|
8197
|
+
handlers: {
|
|
8198
|
+
onPointerDown: (e) => this.#handleNodePointerDown(e),
|
|
8199
|
+
onClick: (n, e) => {
|
|
8200
|
+
e.stopPropagation();
|
|
8201
|
+
this.#handleItemClick(n, e);
|
|
8202
|
+
},
|
|
8203
|
+
onDblClick: (n, e) => {
|
|
8204
|
+
e.stopPropagation();
|
|
8205
|
+
this.#handleItemDblClick(n);
|
|
8206
|
+
},
|
|
8207
|
+
onPointerEnter: (id) => this.tickled.handlePointerEnter(id)
|
|
8208
|
+
}
|
|
8209
|
+
});
|
|
6766
8210
|
}
|
|
6767
8211
|
render() {
|
|
6768
8212
|
const { nodes, edges } = this.#layout();
|
|
6769
|
-
const
|
|
6770
|
-
const
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
}
|
|
6776
|
-
const labelKeys = this.#computeLabelKeys(nodes);
|
|
8213
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
8214
|
+
const branches = branchIndices(this.#effectiveRoot);
|
|
8215
|
+
const ctx = this.#nodeContext(nodes, branches);
|
|
8216
|
+
this.#lastLabelIds = ctx.labelIds;
|
|
8217
|
+
const { zoom, offset } = this.#zoomState();
|
|
8218
|
+
const pan = offset.x === 0 && offset.y === 0 ? `` : `translate(${offset.x * 500} ${offset.y * 500}) `;
|
|
6777
8219
|
return html`
|
|
6778
8220
|
<svg
|
|
6779
8221
|
class="hyperbolictree"
|
|
6780
|
-
viewBox="${-
|
|
8222
|
+
viewBox="${-525} ${-525} ${1050} ${1050}"
|
|
6781
8223
|
preserveAspectRatio="xMidYMid meet"
|
|
6782
8224
|
role="tree"
|
|
6783
8225
|
tabindex="0"
|
|
6784
8226
|
data-input-mode=${this.tickled.inputMode}
|
|
8227
|
+
data-node-shape=${this.nodeShape}
|
|
6785
8228
|
@keydown=${(e) => this.#handleKeyDown(e)}
|
|
6786
|
-
@pointerdown=${(e) => this.#
|
|
6787
|
-
@pointermove=${(e) => this.#
|
|
6788
|
-
@pointerup=${(e) => this.#
|
|
8229
|
+
@pointerdown=${(e) => this.#gestures.onPointerDown(e)}
|
|
8230
|
+
@pointermove=${(e) => this.#gestures.onPointerMove(e)}
|
|
8231
|
+
@pointerup=${(e) => this.#gestures.onPointerUp(e)}
|
|
8232
|
+
@pointercancel=${(e) => this.#gestures.onPointerUp(e)}
|
|
8233
|
+
@wheel=${(e) => this.#gestures.onWheel(e)}
|
|
6789
8234
|
@pointerleave=${() => this.tickled.clearTickled()}
|
|
6790
8235
|
>
|
|
6791
|
-
<g class="zoom" transform="scale(${
|
|
6792
|
-
<circle class="disk-boundary" part="boundary" cx="0" cy="0" r=${
|
|
8236
|
+
<g class="zoom" transform="${pan}scale(${zoom})">
|
|
8237
|
+
<circle class="disk-boundary" part="boundary" cx="0" cy="0" r=${500} />
|
|
6793
8238
|
<g class="edges" part="edges">
|
|
6794
|
-
${
|
|
8239
|
+
${renderEdges(edges, byId, branches, this.edgeRenderer)}
|
|
6795
8240
|
</g>
|
|
6796
8241
|
<g class="nodes" part="nodes">
|
|
6797
|
-
${
|
|
8242
|
+
${this.#renderNodes(nodes, ctx)}
|
|
6798
8243
|
</g>
|
|
6799
8244
|
</g>
|
|
6800
8245
|
</svg>
|
|
8246
|
+
${renderChildPreview(this.#previewFor(nodes), {
|
|
8247
|
+
onPointerEnter: () => this.#preview.pointerEnter(),
|
|
8248
|
+
onPointerLeave: () => this.#preview.pointerLeave(this.tickled.tickledId),
|
|
8249
|
+
onPick: (child) => this.#pickPreviewChild(child)
|
|
8250
|
+
})}
|
|
6801
8251
|
${this.search.renderField()}
|
|
6802
8252
|
`;
|
|
6803
8253
|
}
|
|
@@ -6805,102 +8255,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
6805
8255
|
this.styles = [
|
|
6806
8256
|
themeFallbacks,
|
|
6807
8257
|
listSearchStyles,
|
|
6808
|
-
|
|
6809
|
-
:host {
|
|
6810
|
-
--hyperbolictree-node-color: var(--surface-4);
|
|
6811
|
-
--hyperbolictree-node-text: var(--surface-3-text);
|
|
6812
|
-
--hyperbolictree-edge-color: var(--surface-6);
|
|
6813
|
-
--hyperbolictree-boundary-color: var(--border-subtle);
|
|
6814
|
-
--hyperbolictree-font-size: 25px;
|
|
6815
|
-
|
|
6816
|
-
display: block;
|
|
6817
|
-
overflow: hidden;
|
|
6818
|
-
outline: none;
|
|
6819
|
-
overscroll-behavior: contain;
|
|
6820
|
-
}
|
|
6821
|
-
|
|
6822
|
-
/*
|
|
6823
|
-
* delegatesFocus means the host itself reports as focused whenever
|
|
6824
|
-
* the shadow DOM's focused descendant (the svg) is — without this,
|
|
6825
|
-
* the host also picks up the browser's own default focus ring/fill
|
|
6826
|
-
* sized to the whole box.
|
|
6827
|
-
*/
|
|
6828
|
-
:host(:focus),
|
|
6829
|
-
:host(:focus-visible) {
|
|
6830
|
-
outline: none;
|
|
6831
|
-
}
|
|
6832
|
-
|
|
6833
|
-
.hyperbolictree {
|
|
6834
|
-
display: block;
|
|
6835
|
-
width: 100%;
|
|
6836
|
-
height: 100%;
|
|
6837
|
-
outline: none;
|
|
6838
|
-
touch-action: none;
|
|
6839
|
-
overscroll-behavior: contain;
|
|
6840
|
-
}
|
|
6841
|
-
|
|
6842
|
-
.disk-boundary {
|
|
6843
|
-
fill: none;
|
|
6844
|
-
stroke: var(--hyperbolictree-boundary-color);
|
|
6845
|
-
stroke-width: 1;
|
|
6846
|
-
}
|
|
6847
|
-
|
|
6848
|
-
.edge {
|
|
6849
|
-
fill: none;
|
|
6850
|
-
stroke: var(--hyperbolictree-edge-color);
|
|
6851
|
-
/* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
|
|
6852
|
-
stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
|
|
6853
|
-
}
|
|
6854
|
-
|
|
6855
|
-
.node {
|
|
6856
|
-
cursor: pointer;
|
|
6857
|
-
}
|
|
6858
|
-
|
|
6859
|
-
.node-hit {
|
|
6860
|
-
fill: transparent;
|
|
6861
|
-
pointer-events: all;
|
|
6862
|
-
}
|
|
6863
|
-
|
|
6864
|
-
.node-dot {
|
|
6865
|
-
/* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
|
|
6866
|
-
fill: color-mix(in oklab, var(--hyperbolictree-node-color), var(--accent) calc(var(--node-weight, 0.5) * 50%));
|
|
6867
|
-
stroke: var(--surface-2);
|
|
6868
|
-
stroke-width: 1;
|
|
6869
|
-
pointer-events: none;
|
|
6870
|
-
}
|
|
6871
|
-
|
|
6872
|
-
.node.tickled .node-dot {
|
|
6873
|
-
fill: var(--item-bg-tickled);
|
|
6874
|
-
}
|
|
6875
|
-
|
|
6876
|
-
.node.selected .node-dot {
|
|
6877
|
-
fill: var(--item-bg-selected);
|
|
6878
|
-
}
|
|
6879
|
-
|
|
6880
|
-
.node.selected.tickled .node-dot {
|
|
6881
|
-
fill: var(--item-bg-selected-tickled);
|
|
6882
|
-
}
|
|
6883
|
-
|
|
6884
|
-
.node.focused .node-dot {
|
|
6885
|
-
stroke: var(--accent);
|
|
6886
|
-
stroke-width: 3;
|
|
6887
|
-
}
|
|
6888
|
-
|
|
6889
|
-
.node.filtered {
|
|
6890
|
-
opacity: 0.3;
|
|
6891
|
-
}
|
|
6892
|
-
|
|
6893
|
-
.node.loading .node-dot {
|
|
6894
|
-
opacity: 0.6;
|
|
6895
|
-
}
|
|
6896
|
-
|
|
6897
|
-
.hyperbolictree-label {
|
|
6898
|
-
font-size: var(--hyperbolictree-font-size);
|
|
6899
|
-
fill: var(--hyperbolictree-node-text);
|
|
6900
|
-
cursor: pointer;
|
|
6901
|
-
user-select: none;
|
|
6902
|
-
}
|
|
6903
|
-
`
|
|
8258
|
+
hyperbolicTreeStyles
|
|
6904
8259
|
];
|
|
6905
8260
|
}
|
|
6906
8261
|
};
|
|
@@ -6923,6 +8278,24 @@ __decorate([property({
|
|
|
6923
8278
|
type: Number,
|
|
6924
8279
|
attribute: `max-radius`
|
|
6925
8280
|
})], HyperbolicTreeListElement.prototype, "maxRadius", void 0);
|
|
8281
|
+
__decorate([property({
|
|
8282
|
+
type: String,
|
|
8283
|
+
attribute: `node-shape`,
|
|
8284
|
+
reflect: true
|
|
8285
|
+
})], HyperbolicTreeListElement.prototype, "nodeShape", void 0);
|
|
8286
|
+
__decorate([property({
|
|
8287
|
+
type: Number,
|
|
8288
|
+
attribute: `box-max-chars`
|
|
8289
|
+
})], HyperbolicTreeListElement.prototype, "boxMaxChars", void 0);
|
|
8290
|
+
__decorate([property({
|
|
8291
|
+
type: Number,
|
|
8292
|
+
attribute: `min-label-size`
|
|
8293
|
+
})], HyperbolicTreeListElement.prototype, "minLabelSize", void 0);
|
|
8294
|
+
__decorate([property({
|
|
8295
|
+
type: Boolean,
|
|
8296
|
+
attribute: `color-by-branch`,
|
|
8297
|
+
reflect: true
|
|
8298
|
+
})], HyperbolicTreeListElement.prototype, "colorByBranch", void 0);
|
|
6926
8299
|
__decorate([property({
|
|
6927
8300
|
type: Number,
|
|
6928
8301
|
attribute: `node-radius`
|
|
@@ -6940,7 +8313,23 @@ __decorate([property({
|
|
|
6940
8313
|
attribute: `max-weight-scale`
|
|
6941
8314
|
})], HyperbolicTreeListElement.prototype, "maxWeightScale", void 0);
|
|
6942
8315
|
__decorate([property({ type: Number })], HyperbolicTreeListElement.prototype, "zoom", void 0);
|
|
8316
|
+
__decorate([property({
|
|
8317
|
+
type: Boolean,
|
|
8318
|
+
attribute: `disable-zoom-gestures`
|
|
8319
|
+
})], HyperbolicTreeListElement.prototype, "disableZoomGestures", void 0);
|
|
8320
|
+
__decorate([property({
|
|
8321
|
+
type: Boolean,
|
|
8322
|
+
attribute: `disable-radial-labels`
|
|
8323
|
+
})], HyperbolicTreeListElement.prototype, "disableRadialLabels", void 0);
|
|
8324
|
+
__decorate([property({
|
|
8325
|
+
type: Boolean,
|
|
8326
|
+
attribute: `disable-child-preview`
|
|
8327
|
+
})], HyperbolicTreeListElement.prototype, "disableChildPreview", void 0);
|
|
6943
8328
|
__decorate([property({ type: Number })], HyperbolicTreeListElement.prototype, "curvature", void 0);
|
|
8329
|
+
__decorate([property({
|
|
8330
|
+
type: Number,
|
|
8331
|
+
attribute: `sibling-spacing`
|
|
8332
|
+
})], HyperbolicTreeListElement.prototype, "siblingSpacing", void 0);
|
|
6944
8333
|
__decorate([property({
|
|
6945
8334
|
type: Number,
|
|
6946
8335
|
attribute: `focus-animation-duration`
|
|
@@ -6959,9 +8348,12 @@ __decorate([property({
|
|
|
6959
8348
|
attribute: `search-modes`,
|
|
6960
8349
|
reflect: true
|
|
6961
8350
|
})], HyperbolicTreeListElement.prototype, "searchModes", void 0);
|
|
6962
|
-
__decorate([state()], HyperbolicTreeListElement.prototype, "
|
|
6963
|
-
__decorate([state()], HyperbolicTreeListElement.prototype, "
|
|
6964
|
-
__decorate([state()], HyperbolicTreeListElement.prototype, "
|
|
8351
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_focusId", void 0);
|
|
8352
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_viewOverride", void 0);
|
|
8353
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_focusRotation", void 0);
|
|
8354
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_zoomOffset", void 0);
|
|
8355
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_previewId", void 0);
|
|
8356
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_hostSize", void 0);
|
|
6965
8357
|
HyperbolicTreeListElement = __decorate([safeCustomElement(`ixfx-hyperbolictree-list`)], HyperbolicTreeListElement);
|
|
6966
8358
|
//#endregion
|
|
6967
8359
|
//#region src/icicle-tree/layout.ts
|
|
@@ -14356,6 +15748,6 @@ function init() {
|
|
|
14356
15748
|
console.log(`Init`);
|
|
14357
15749
|
}
|
|
14358
15750
|
//#endregion
|
|
14359
|
-
export { AcTextElement, AcTokenElement, BottomDrawerElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GridListElement, GroupedItemListerElement, HyperbolicTreeListElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IcicleTreeListElement, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LIST_SEARCH_COMMAND_IDS, LIST_SEARCH_MODES, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListDragSelectController, ListNativeDragController, ListSearchController, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, MulticolListElement, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, ShapefillValueElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeKeyboardNavController, TreeListElement, TreemapListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applySnapshot, applyTreeDrop, attachTimelineInteractions, bottomDrawerCommandId, buildHighlightTargets, buildModel, captionForEntry, clampToDisk, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, computeCanonicalPositions, computeColour, computeHyperbolicLayout, computeIcicleLayout, computeLeafGeoms, computeTreemapLayout, createAcCompletionProvider, createDataListSearchAdapter, createDefaultListDragPreview, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createVerticalListAdapter, distribute, dropTargetFor, euclideanRadiusForHyperbolicDistance, extractStack, findHyperbolicNode, findHyperbolicPath, findIcicleNode, findIciclePath, findNode, findPanelNode, findParentOf, findStackOf, findStackPathByName, findTreemapNode, findTreemapPath, geodesicInterpolate, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, hyperbolicDistanceForEuclideanRadius, iconBus, init, insertBeside, isDragging, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, listSearchStyles, mobiusAdd, mobiusFromOrigin, mobiusToOrigin, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, norm, normalise, normaliseValue, normalize, panelGapIndex, parseColourScaleAttr, parseLayout, poincareDistance, pointerOverAnyHost, prefersReducedMotion, preserveRestoreWeights, pruneDraggedDescendants, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, serializeLayout, setActive, setIconFont, setPanelRestoreWeight, startListItemOutAnimation, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, toolbarGapIndex, walk, wrapRoot };
|
|
15751
|
+
export { AcTextElement, AcTokenElement, BottomDrawerElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DEFAULT_SIBLING_SPACING, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GridListElement, GroupedItemListerElement, HyperbolicTreeListElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IcicleTreeListElement, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LIST_SEARCH_COMMAND_IDS, LIST_SEARCH_MODES, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListDragSelectController, ListNativeDragController, ListSearchController, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, MulticolListElement, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, ShapefillValueElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeKeyboardNavController, TreeListElement, TreemapListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applyIsometry, applySnapshot, applyTreeDrop, attachTimelineInteractions, bottomDrawerCommandId, buildHighlightTargets, buildModel, captionForEntry, clampToDisk, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, composeIsometry, computeCanonicalPositions, computeColour, computeHyperbolicLayout, computeIcicleLayout, computeLeafGeoms, computeTreemapLayout, createAcCompletionProvider, createDataListSearchAdapter, createDefaultListDragPreview, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createVerticalListAdapter, decomposeViewIsometry, distribute, dropTargetFor, euclideanRadiusForHyperbolicDistance, extractStack, findHyperbolicIdOf, findHyperbolicKeyPath, findHyperbolicNode, findHyperbolicPath, findHyperbolicPathById, findIcicleNode, findIciclePath, findNode, findPanelNode, findParentOf, findStackOf, findStackPathByName, findTreemapNode, findTreemapPath, geodesicInterpolate, geodesicPath, geodesicSegment, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, hyperbolicBranchId, hyperbolicChildIds, hyperbolicDistanceForEuclideanRadius, hyperbolicIdDepth, hyperbolicRelativeFrame, iconBus, identityIsometry, init, insertBeside, invertIsometry, isDragging, isHyperbolicDescendantId, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, listSearchStyles, mobiusAdd, mobiusFromOrigin, mobiusToOrigin, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, norm, normalise, normaliseValue, normalize, panelGapIndex, parseColourScaleAttr, parseLayout, poincareDistance, pointerOverAnyHost, prefersReducedMotion, preserveRestoreWeights, pruneDraggedDescendants, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, rotationIsometry, serializeLayout, setActive, setIconFont, setPanelRestoreWeight, startListItemOutAnimation, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, toolbarGapIndex, translationFromOrigin, viewIsometry, walk, wrapRoot };
|
|
14360
15752
|
|
|
14361
15753
|
//# sourceMappingURL=index.js.map
|