@behindthemusictree/genre-tree-view 6.2.0 → 6.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +447 -178
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -3
- package/dist/index.d.ts +23 -3
- package/dist/index.js +454 -185
- package/dist/index.js.map +1 -1
- package/dist/styles.css +104 -47
- package/package.json +4 -2
package/dist/index.cjs
CHANGED
|
@@ -181,6 +181,9 @@ var MIN_NODE_WIDTH = 180;
|
|
|
181
181
|
var MAX_NODE_WIDTH = 700;
|
|
182
182
|
var MIN_NODE_HEIGHT = 35;
|
|
183
183
|
var MAX_NODE_HEIGHT = 120;
|
|
184
|
+
var NODE_SIZE_REFERENCE_COUNT = 100;
|
|
185
|
+
var HIGH_DENSITY_NODE_MAX_FLOOR_SCALE = 1.5;
|
|
186
|
+
var LOW_DENSITY_DOT_MAX_RADIUS = 70;
|
|
184
187
|
var TOOLBAR_MENU_X_GAP = 4;
|
|
185
188
|
var ACTIONS_OVERLAY_WIDTH = MENU_WIDTH + TOOLBAR_MENU_X_GAP;
|
|
186
189
|
var ACTIONS_OVERLAY_HEIGHT = RECT_BASE_DIMENSIONS.HEIGHT * 7;
|
|
@@ -195,23 +198,47 @@ var HORIZONTAL_SEPARATION_BETWEEN_NODES = (NODE_DIMENSIONS.WIDTH + HORIZONTAL_SE
|
|
|
195
198
|
var VERTICAL_SEPARATION_BETWEEN_NODES = NODE_DIMENSIONS.HEIGHT + VERTICAL_SEPARATION_BETWEEN_RECTANGLES;
|
|
196
199
|
var VERTICAL_ORIENTATION_DEPTH_SEPARATION = VERTICAL_SEPARATION_BETWEEN_NODES * 2 * DEPTH_AXIS_SPACING_FACTOR;
|
|
197
200
|
var SIBLING_SEPARATION_BETWEEN_NODES = MAX_NODE_WIDTH + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES;
|
|
198
|
-
function
|
|
199
|
-
|
|
201
|
+
function getDensityAdjustedMax(baseMax, min, nodeCount) {
|
|
202
|
+
const safeNodeCount = Number.isFinite(nodeCount) ? Math.max(1, nodeCount) : 1;
|
|
203
|
+
const availableRangeScale = Math.min(1, NODE_SIZE_REFERENCE_COUNT / safeNodeCount);
|
|
204
|
+
const denseTreeFloor = min * HIGH_DENSITY_NODE_MAX_FLOOR_SCALE;
|
|
205
|
+
return denseTreeFloor + (baseMax - denseTreeFloor) * availableRangeScale;
|
|
206
|
+
}
|
|
207
|
+
function getDensityAdjustedDotMax(nodeCount) {
|
|
208
|
+
const safeNodeCount = Number.isFinite(nodeCount) ? Math.max(1, nodeCount) : 1;
|
|
209
|
+
const inverseDensityRadius = NODE_DOT_MAX_RADIUS * NODE_SIZE_REFERENCE_COUNT / safeNodeCount;
|
|
210
|
+
const denseTreeFloor = NODE_DOT_MIN_RADIUS * HIGH_DENSITY_NODE_MAX_FLOOR_SCALE;
|
|
211
|
+
return Math.max(denseTreeFloor, Math.min(LOW_DENSITY_DOT_MAX_RADIUS, inverseDensityRadius));
|
|
212
|
+
}
|
|
213
|
+
function getItemCountRange(nodes, options = {}) {
|
|
200
214
|
const counts = nodes.map((node) => node.itemCount);
|
|
201
|
-
|
|
215
|
+
const validScaleMax = options.itemCountScaleMax !== void 0 && Number.isFinite(options.itemCountScaleMax) && options.itemCountScaleMax >= 0;
|
|
216
|
+
const nodeCount = options.nodeCountForSizing ?? nodes.length;
|
|
217
|
+
const observedMin = counts.length === 0 ? 0 : Math.min(...counts);
|
|
218
|
+
const observedMax = counts.length === 0 ? 0 : Math.max(...counts);
|
|
219
|
+
const includeDensityCaps = options.nodeCountForSizing !== void 0 || validScaleMax;
|
|
220
|
+
return {
|
|
221
|
+
min: validScaleMax ? 0 : observedMin,
|
|
222
|
+
max: validScaleMax ? options.itemCountScaleMax : observedMax,
|
|
223
|
+
...includeDensityCaps && {
|
|
224
|
+
maxNodeWidth: getDensityAdjustedMax(MAX_NODE_WIDTH, MIN_NODE_WIDTH, nodeCount),
|
|
225
|
+
maxNodeHeight: getDensityAdjustedMax(MAX_NODE_HEIGHT, MIN_NODE_HEIGHT, nodeCount),
|
|
226
|
+
maxDotRadius: getDensityAdjustedDotMax(nodeCount)
|
|
227
|
+
}
|
|
228
|
+
};
|
|
202
229
|
}
|
|
203
230
|
function logarithmicPosition(itemCount, range) {
|
|
204
231
|
const { min, max } = range;
|
|
205
232
|
const logMin = Math.log1p(Math.max(min, 0));
|
|
206
233
|
const logMax = Math.log1p(Math.max(max, 0));
|
|
207
234
|
const logValue = Math.log1p(Math.max(itemCount, 0));
|
|
208
|
-
return logMax > logMin ? (logValue - logMin) / (logMax - logMin) : 0;
|
|
235
|
+
return logMax > logMin ? Math.max(0, Math.min(1, (logValue - logMin) / (logMax - logMin))) : 0;
|
|
209
236
|
}
|
|
210
237
|
function calculateNodeDimensions(itemCount, range) {
|
|
211
238
|
const t = logarithmicPosition(itemCount, range);
|
|
212
239
|
return {
|
|
213
|
-
WIDTH: Math.round(MIN_NODE_WIDTH + t * (MAX_NODE_WIDTH - MIN_NODE_WIDTH)),
|
|
214
|
-
HEIGHT: Math.round(MIN_NODE_HEIGHT + t * (MAX_NODE_HEIGHT - MIN_NODE_HEIGHT))
|
|
240
|
+
WIDTH: Math.round(MIN_NODE_WIDTH + t * ((range.maxNodeWidth ?? MAX_NODE_WIDTH) - MIN_NODE_WIDTH)),
|
|
241
|
+
HEIGHT: Math.round(MIN_NODE_HEIGHT + t * ((range.maxNodeHeight ?? MAX_NODE_HEIGHT) - MIN_NODE_HEIGHT))
|
|
215
242
|
};
|
|
216
243
|
}
|
|
217
244
|
var NODE_MIN_BORDER_WIDTH = 1;
|
|
@@ -256,7 +283,7 @@ var NODE_DOT_LABEL_GAP = 6;
|
|
|
256
283
|
var NODE_LAYOUT_SLOT_RADIUS = 8;
|
|
257
284
|
function calculateNodeDotRadius(itemCount, range) {
|
|
258
285
|
const minArea = NODE_DOT_MIN_RADIUS ** 2;
|
|
259
|
-
const maxArea = NODE_DOT_MAX_RADIUS ** 2;
|
|
286
|
+
const maxArea = (range.maxDotRadius ?? NODE_DOT_MAX_RADIUS) ** 2;
|
|
260
287
|
return Math.sqrt(minArea + logarithmicPosition(itemCount, range) * (maxArea - minArea));
|
|
261
288
|
}
|
|
262
289
|
var WHEEL_RADIUS = 260;
|
|
@@ -291,19 +318,22 @@ var PAN_MIN_VISIBLE_PX = 80;
|
|
|
291
318
|
var PAN_ZOOM_SETTLE_MS = 100;
|
|
292
319
|
var DEFAULT_VIEWPORT_CENTER_GUARD_MS = 1e3;
|
|
293
320
|
var INFO_PANEL_WIDTH = 280;
|
|
321
|
+
var INFO_PANEL_SHEET_MAX_WIDTH = 767;
|
|
322
|
+
var INFO_PANEL_SHEET_PEEK_FRACTION = 0.4;
|
|
323
|
+
var INFO_PANEL_SHEET_DRAG_THRESHOLD_PX = 32;
|
|
294
324
|
|
|
295
325
|
// src/NodeHelper.tsx
|
|
296
326
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
297
327
|
function buildTreeHierarchyStructure(d3Lib, nodes) {
|
|
298
328
|
return d3Lib.stratify().id((d) => d.id).parentId((d) => d.parentId)(nodes);
|
|
299
329
|
}
|
|
300
|
-
function calculateLocalRootDimensions(d3Lib, nodes) {
|
|
330
|
+
function calculateLocalRootDimensions(d3Lib, nodes, options = {}) {
|
|
301
331
|
const root = buildTreeHierarchyStructure(d3Lib, nodes);
|
|
302
|
-
const range = getItemCountRange(root.descendants().map((d) => d.data));
|
|
332
|
+
const range = getItemCountRange(root.descendants().map((d) => d.data), options);
|
|
303
333
|
return calculateNodeDimensions(root.data.itemCount, range);
|
|
304
334
|
}
|
|
305
|
-
function calculateRootAnchorClearance(d3Lib, nodes, rootItemCount, rootItemCountRange, axis) {
|
|
306
|
-
const localExtent = calculateLocalRootDimensions(d3Lib, nodes)[axis];
|
|
335
|
+
function calculateRootAnchorClearance(d3Lib, nodes, rootItemCount, rootItemCountRange, axis, options = {}) {
|
|
336
|
+
const localExtent = calculateLocalRootDimensions(d3Lib, nodes, options)[axis];
|
|
307
337
|
const displayExtent = calculateNodeDimensions(rootItemCount, rootItemCountRange)[axis];
|
|
308
338
|
return (localExtent + displayExtent) / 2;
|
|
309
339
|
}
|
|
@@ -387,13 +417,31 @@ function appendPaths(d3Lib, svg, treeData, itemCountRange, orientation = "horizo
|
|
|
387
417
|
}
|
|
388
418
|
|
|
389
419
|
// src/tree-renderer.ts
|
|
390
|
-
function
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
const
|
|
420
|
+
function getLayoutSpacings(itemCountRange, depthSpacingScale = 1) {
|
|
421
|
+
const maxWidth = itemCountRange.maxNodeWidth ?? MAX_NODE_WIDTH;
|
|
422
|
+
const maxHeight = itemCountRange.maxNodeHeight ?? MAX_NODE_HEIGHT;
|
|
423
|
+
const verticalSeparation = maxHeight + VERTICAL_SEPARATION_BETWEEN_RECTANGLES;
|
|
424
|
+
return {
|
|
425
|
+
horizontalDepth: (maxWidth + ACTIONS_OVERLAY_WIDTH + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES) * DEPTH_AXIS_SPACING_FACTOR * depthSpacingScale,
|
|
426
|
+
verticalBreadth: verticalSeparation,
|
|
427
|
+
verticalDepth: verticalSeparation * 2 * DEPTH_AXIS_SPACING_FACTOR * depthSpacingScale,
|
|
428
|
+
verticalSibling: maxWidth + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function calculateSvgDimensions(d3Lib, treeData, orientation = "horizontal", hideRoot = false, depthSpacingScale = 1, suppliedItemCountRange) {
|
|
432
|
+
const treeNodes = treeData.descendants();
|
|
433
|
+
const nodes = treeNodes;
|
|
434
|
+
const itemCountRange = suppliedItemCountRange ?? getItemCountRange(
|
|
435
|
+
treeNodes.map((d) => d.data)
|
|
436
|
+
);
|
|
437
|
+
const maxNodeDimensions = {
|
|
438
|
+
WIDTH: itemCountRange.maxNodeWidth ?? MAX_NODE_WIDTH,
|
|
439
|
+
HEIGHT: itemCountRange.maxNodeHeight ?? MAX_NODE_HEIGHT
|
|
440
|
+
};
|
|
394
441
|
const maximumLevel = d3Lib.max(nodes, (d) => d.depth);
|
|
395
|
-
const
|
|
396
|
-
const
|
|
442
|
+
const layoutSpacings = getLayoutSpacings(itemCountRange, depthSpacingScale);
|
|
443
|
+
const depthSeparation = layoutSpacings.verticalDepth;
|
|
444
|
+
const depthSeparationHorizontal = layoutSpacings.horizontalDepth;
|
|
397
445
|
if (isVerticalOrientation(orientation)) {
|
|
398
446
|
const svgHeight2 = hideRoot ? maximumLevel * depthSeparation : maximumLevel * depthSeparation + maxNodeDimensions.HEIGHT + ACTIONS_OVERLAY_HEIGHT / 2;
|
|
399
447
|
const rootWidth = calculateNodeDimensions(
|
|
@@ -455,10 +503,15 @@ function calculateSvgDimensions(d3Lib, treeData, orientation = "horizontal", hid
|
|
|
455
503
|
const svgWidth = maximumLevel * depthSeparationHorizontal + maxNodeDimensions.WIDTH + ACTIONS_OVERLAY_WIDTH;
|
|
456
504
|
return { svgWidth, svgHeight, highestVerticalCoordinate, rootDepthOffset: 0 };
|
|
457
505
|
}
|
|
458
|
-
function setupTreeLayout(d3Lib, treeData, highestVerticalCoordinate, orientation = "horizontal", rootDepthOffset = 0, svgWidth = 0, depthSpacingScale = 1) {
|
|
506
|
+
function setupTreeLayout(d3Lib, treeData, highestVerticalCoordinate, orientation = "horizontal", rootDepthOffset = 0, svgWidth = 0, depthSpacingScale = 1, suppliedItemCountRange) {
|
|
507
|
+
const treeNodes = treeData.descendants();
|
|
508
|
+
const itemCountRange = suppliedItemCountRange ?? getItemCountRange(
|
|
509
|
+
treeNodes.map((d) => d.data)
|
|
510
|
+
);
|
|
511
|
+
const layoutSpacings = getLayoutSpacings(itemCountRange, depthSpacingScale);
|
|
459
512
|
if (isVerticalOrientation(orientation)) {
|
|
460
513
|
const maximumLevel = d3Lib.max(treeData.descendants(), (d) => d.depth);
|
|
461
|
-
const depthSeparation =
|
|
514
|
+
const depthSeparation = layoutSpacings.verticalDepth;
|
|
462
515
|
treeData.each(function(d) {
|
|
463
516
|
d.x = d.x + highestVerticalCoordinate;
|
|
464
517
|
d.y = depthAxisSign(orientation) === -1 ? (maximumLevel - d.depth) * depthSeparation : d.depth * depthSeparation + rootDepthOffset;
|
|
@@ -480,24 +533,30 @@ function setupTreeLayout(d3Lib, treeData, highestVerticalCoordinate, orientation
|
|
|
480
533
|
});
|
|
481
534
|
return treeData;
|
|
482
535
|
}
|
|
483
|
-
function createTreeLayout(d3Lib, root, orientation = "horizontal", depthSpacingScale = 1) {
|
|
536
|
+
function createTreeLayout(d3Lib, root, orientation = "horizontal", depthSpacingScale = 1, suppliedItemCountRange) {
|
|
537
|
+
const nodes = root.descendants();
|
|
538
|
+
const itemCountRange = suppliedItemCountRange ?? getItemCountRange(
|
|
539
|
+
nodes.map((d) => d.data)
|
|
540
|
+
);
|
|
541
|
+
const layoutSpacings = getLayoutSpacings(itemCountRange, depthSpacingScale);
|
|
484
542
|
const nodeSize = isVerticalOrientation(orientation) ? [
|
|
485
|
-
|
|
486
|
-
|
|
543
|
+
layoutSpacings.verticalSibling,
|
|
544
|
+
layoutSpacings.verticalDepth
|
|
487
545
|
] : [
|
|
488
|
-
|
|
489
|
-
|
|
546
|
+
layoutSpacings.verticalBreadth,
|
|
547
|
+
layoutSpacings.horizontalDepth
|
|
490
548
|
];
|
|
491
549
|
const treeLayout = d3Lib.tree().nodeSize(nodeSize).separation(() => 1);
|
|
492
550
|
return treeLayout(root);
|
|
493
551
|
}
|
|
494
|
-
function renderTree(d3Lib, svgRef, treeData, svgWidth, svgHeight, reparentingNodeId, reparentForbiddenIds, rootColor, callbacks, orientation = "horizontal", hideRoot = false, selectedNodeId = null) {
|
|
552
|
+
function renderTree(d3Lib, svgRef, treeData, svgWidth, svgHeight, reparentingNodeId, reparentForbiddenIds, rootColor, callbacks, orientation = "horizontal", hideRoot = false, selectedNodeId = null, suppliedItemCountRange) {
|
|
495
553
|
const { onReparentTargetSelect, onNodeClick } = callbacks;
|
|
496
554
|
if (!svgRef.current) {
|
|
497
555
|
throw new Error("SVG reference is null");
|
|
498
556
|
}
|
|
499
|
-
const
|
|
500
|
-
|
|
557
|
+
const treeNodes = treeData.descendants();
|
|
558
|
+
const itemCountRange = suppliedItemCountRange ?? getItemCountRange(
|
|
559
|
+
treeNodes.map((d) => d.data)
|
|
501
560
|
);
|
|
502
561
|
const svg = d3Lib.select(svgRef.current).append("svg").attr("width", svgWidth).attr("height", svgHeight).style("overflow", "visible").append("g");
|
|
503
562
|
const safeRootId = treeData.data.id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -645,7 +704,7 @@ function renderTree(d3Lib, svgRef, treeData, svgWidth, svgHeight, reparentingNod
|
|
|
645
704
|
}
|
|
646
705
|
|
|
647
706
|
// src/FloatingControls.tsx
|
|
648
|
-
var
|
|
707
|
+
var import_ui = require("@behindthemusictree/ui");
|
|
649
708
|
|
|
650
709
|
// src/BusyIndicator.tsx
|
|
651
710
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
@@ -658,7 +717,7 @@ function BusyIndicator() {
|
|
|
658
717
|
|
|
659
718
|
// src/FloatingControls.tsx
|
|
660
719
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
661
|
-
function FloatingControls({ panZoom, onFit, orientation, leading, busy }) {
|
|
720
|
+
function FloatingControls({ panZoom, onFit, orientation, zoomControls, leading, busy }) {
|
|
662
721
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
663
722
|
"div",
|
|
664
723
|
{
|
|
@@ -669,31 +728,18 @@ function FloatingControls({ panZoom, onFit, orientation, leading, busy }) {
|
|
|
669
728
|
children: [
|
|
670
729
|
busy && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BusyIndicator, {}),
|
|
671
730
|
leading && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "gtv-floating-controls-leading", children: leading }),
|
|
672
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
685
|
-
"button",
|
|
686
|
-
{
|
|
687
|
-
type: "button",
|
|
688
|
-
className: ["gtv-zoom-btn", !panZoom.canZoomOut && "gtv-zoom-btn--disabled"].filter(Boolean).join(" "),
|
|
689
|
-
disabled: !panZoom.canZoomOut,
|
|
690
|
-
onClick: panZoom.zoomOut,
|
|
691
|
-
"aria-label": "Zoom out",
|
|
692
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_md.MdZoomOut, { className: "gtv-icon", size: 18 })
|
|
693
|
-
}
|
|
694
|
-
),
|
|
695
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "gtv-zoom-btn", onClick: onFit, "aria-label": "Fit to frame", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_md.MdFitScreen, { className: "gtv-icon", size: 18 }) })
|
|
696
|
-
] })
|
|
731
|
+
zoomControls !== "never" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
732
|
+
import_ui.ZoomControls,
|
|
733
|
+
{
|
|
734
|
+
onZoomIn: panZoom.zoomIn,
|
|
735
|
+
onZoomOut: panZoom.zoomOut,
|
|
736
|
+
onFit,
|
|
737
|
+
canZoomIn: panZoom.canZoomIn,
|
|
738
|
+
canZoomOut: panZoom.canZoomOut,
|
|
739
|
+
orientation,
|
|
740
|
+
className: zoomControls === "auto" ? "gtv-zoom-controls gtv-zoom-controls--auto" : "gtv-zoom-controls"
|
|
741
|
+
}
|
|
742
|
+
)
|
|
697
743
|
]
|
|
698
744
|
}
|
|
699
745
|
);
|
|
@@ -757,6 +803,18 @@ function computeFitScale(contentWidth, contentHeight, viewportWidth, viewportHei
|
|
|
757
803
|
);
|
|
758
804
|
}
|
|
759
805
|
|
|
806
|
+
// src/info-panel-geometry.ts
|
|
807
|
+
function resolveInfoPanelSide() {
|
|
808
|
+
return "left";
|
|
809
|
+
}
|
|
810
|
+
function resolveInfoPanelObscuredArea(element, viewport, panelWidth) {
|
|
811
|
+
if (!element || !viewport) return null;
|
|
812
|
+
if (viewport.clientWidth > 0 && viewport.clientWidth <= INFO_PANEL_SHEET_MAX_WIDTH) {
|
|
813
|
+
return { side: "bottom", height: viewport.clientHeight * INFO_PANEL_SHEET_PEEK_FRACTION };
|
|
814
|
+
}
|
|
815
|
+
return { width: panelWidth, side: resolveInfoPanelSide() };
|
|
816
|
+
}
|
|
817
|
+
|
|
760
818
|
// src/use-pan-zoom.ts
|
|
761
819
|
function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
762
820
|
const [panX, setPanX] = (0, import_react.useState)(0);
|
|
@@ -767,6 +825,17 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
767
825
|
const settleTimerRef = (0, import_react.useRef)(null);
|
|
768
826
|
const stageAnimationRef = (0, import_react.useRef)(null);
|
|
769
827
|
const defaultViewportRef = (0, import_react.useRef)(options.defaultViewport);
|
|
828
|
+
const operationRef = (0, import_react.useRef)(0);
|
|
829
|
+
const restoredSelectionRef = (0, import_react.useRef)(void 0);
|
|
830
|
+
const restoredRevisionRef = (0, import_react.useRef)(void 0);
|
|
831
|
+
const restoredFrameRef = (0, import_react.useRef)(void 0);
|
|
832
|
+
const pendingRestoreRef = (0, import_react.useRef)(null);
|
|
833
|
+
const measuredFrameRef = (0, import_react.useRef)(void 0);
|
|
834
|
+
const fittedFrameRef = (0, import_react.useRef)(null);
|
|
835
|
+
const frameElementsRef = (0, import_react.useRef)([]);
|
|
836
|
+
const retryFrameRef = (0, import_react.useRef)(() => {
|
|
837
|
+
});
|
|
838
|
+
const pendingCenterRef = (0, import_react.useRef)(null);
|
|
770
839
|
const centerGuardUntilRef = (0, import_react.useRef)(0);
|
|
771
840
|
const onViewportChangeRef = (0, import_react.useRef)(options.onViewportChange);
|
|
772
841
|
(0, import_react.useEffect)(() => {
|
|
@@ -840,6 +909,7 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
840
909
|
}, [viewportRef]);
|
|
841
910
|
const zoomAtPoint = (0, import_react.useCallback)(
|
|
842
911
|
(computeNewScale, clientX, clientY) => {
|
|
912
|
+
operationRef.current += 1;
|
|
843
913
|
const viewport = viewportRef.current;
|
|
844
914
|
if (!viewport) {
|
|
845
915
|
return;
|
|
@@ -923,19 +993,26 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
923
993
|
const viewport = viewportRef.current;
|
|
924
994
|
if (!viewport) return;
|
|
925
995
|
const observer = new ResizeObserver((entries) => {
|
|
926
|
-
const entry = entries[0];
|
|
927
|
-
if (!entry)
|
|
996
|
+
const entry = entries?.[0];
|
|
997
|
+
if (!entry) {
|
|
998
|
+
retryFrameRef.current();
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
928
1001
|
const { width, height } = entry.contentRect;
|
|
929
1002
|
const previous = viewportSizeRef.current;
|
|
930
1003
|
viewportSizeRef.current = { width, height };
|
|
931
|
-
if (!previous)
|
|
1004
|
+
if (!previous || previous.width <= 0 || previous.height <= 0) {
|
|
1005
|
+
retryFrameRef.current();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
932
1008
|
const dx = (width - previous.width) / 2;
|
|
933
1009
|
const dy = (height - previous.height) / 2;
|
|
934
1010
|
if (dx === 0 && dy === 0) return;
|
|
935
1011
|
const view = viewRef.current;
|
|
936
1012
|
view.x = clampPanAxis(view.x + dx, view.k, "x");
|
|
937
1013
|
view.y = clampPanAxis(view.y + dy, view.k, "y");
|
|
938
|
-
commitView();
|
|
1014
|
+
commitView(false);
|
|
1015
|
+
retryFrameRef.current();
|
|
939
1016
|
});
|
|
940
1017
|
observer.observe(viewport);
|
|
941
1018
|
return () => observer.disconnect();
|
|
@@ -946,6 +1023,7 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
946
1023
|
return;
|
|
947
1024
|
}
|
|
948
1025
|
const handleWheel = (event) => {
|
|
1026
|
+
operationRef.current += 1;
|
|
949
1027
|
if (event.target.closest(".gtv-info-panel") !== null) {
|
|
950
1028
|
return;
|
|
951
1029
|
}
|
|
@@ -984,6 +1062,7 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
984
1062
|
}, [viewportRef, wheelZoom]);
|
|
985
1063
|
const zoomByButton = (0, import_react.useCallback)(
|
|
986
1064
|
(direction) => {
|
|
1065
|
+
operationRef.current += 1;
|
|
987
1066
|
const viewport = viewportRef.current;
|
|
988
1067
|
if (!viewport) return;
|
|
989
1068
|
const rect = viewport.getBoundingClientRect();
|
|
@@ -998,13 +1077,14 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
998
1077
|
);
|
|
999
1078
|
const fitToFrame = (0, import_react.useCallback)(
|
|
1000
1079
|
(elements, { silent = false } = {}) => {
|
|
1080
|
+
if (!silent) operationRef.current += 1;
|
|
1081
|
+
frameElementsRef.current = elements;
|
|
1001
1082
|
const viewport = viewportRef.current;
|
|
1002
1083
|
const present = elements.filter((el) => Boolean(el));
|
|
1003
|
-
if (!viewport || present.length === 0) return;
|
|
1084
|
+
if (!viewport || present.length === 0) return false;
|
|
1004
1085
|
const viewportRect = viewport.getBoundingClientRect();
|
|
1005
|
-
if (viewportRect.width <= 0 || viewportRect.height <= 0) return;
|
|
1006
|
-
|
|
1007
|
-
if (viewportRect.width <= ZOOM_FIT_PADDING * 2 || viewportRect.height <= ZOOM_FIT_PADDING * 2) return;
|
|
1086
|
+
if (viewportRect.width <= 0 || viewportRect.height <= 0) return false;
|
|
1087
|
+
if (viewportRect.width <= ZOOM_FIT_PADDING * 2 || viewportRect.height <= ZOOM_FIT_PADDING * 2) return false;
|
|
1008
1088
|
const rects = present.map((el) => el.getBoundingClientRect());
|
|
1009
1089
|
const contentLeft = Math.min(...rects.map((r) => r.left));
|
|
1010
1090
|
const contentTop = Math.min(...rects.map((r) => r.top));
|
|
@@ -1013,7 +1093,7 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1013
1093
|
const view = viewRef.current;
|
|
1014
1094
|
const contentWidth = (contentRight - contentLeft) / view.k;
|
|
1015
1095
|
const contentHeight = (contentBottom - contentTop) / view.k;
|
|
1016
|
-
if (contentWidth <= 0 || contentHeight <= 0) return;
|
|
1096
|
+
if (contentWidth <= 0 || contentHeight <= 0) return false;
|
|
1017
1097
|
const contentOriginX = (contentLeft - viewportRect.left - view.x) / view.k;
|
|
1018
1098
|
const contentOriginY = (contentTop - viewportRect.top - view.y) / view.k;
|
|
1019
1099
|
contentBoundsRef.current = {
|
|
@@ -1032,6 +1112,41 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1032
1112
|
const newMinScale = Math.min(ZOOM_MIN_SCALE, fitScale);
|
|
1033
1113
|
minScaleRef.current = newMinScale;
|
|
1034
1114
|
setMinScale(newMinScale);
|
|
1115
|
+
measuredFrameRef.current = options.frameKey;
|
|
1116
|
+
const pending = pendingRestoreRef.current;
|
|
1117
|
+
if (pending && pending.operation !== operationRef.current) {
|
|
1118
|
+
pendingRestoreRef.current = null;
|
|
1119
|
+
pendingCenterRef.current = null;
|
|
1120
|
+
fittedFrameRef.current = { frame: options.frameKey };
|
|
1121
|
+
return true;
|
|
1122
|
+
}
|
|
1123
|
+
if (pending && pending.frame === options.frameKey && pending.operation === operationRef.current) {
|
|
1124
|
+
pendingRestoreRef.current = null;
|
|
1125
|
+
restoredRevisionRef.current = pending.revision;
|
|
1126
|
+
restoredFrameRef.current = pending.frame;
|
|
1127
|
+
restoredSelectionRef.current = options.selectedNodeId;
|
|
1128
|
+
defaultViewportRef.current = void 0;
|
|
1129
|
+
viewRef.current = viewportToView({ ...pending.viewport, zoom: clampZoomScale(pending.viewport.zoom, newMinScale) }, viewportRect.width, viewportRect.height);
|
|
1130
|
+
fittedFrameRef.current = { frame: options.frameKey };
|
|
1131
|
+
pendingCenterRef.current = null;
|
|
1132
|
+
commitView(false);
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
if (silent && fittedFrameRef.current && fittedFrameRef.current.frame === options.frameKey) {
|
|
1136
|
+
const center2 = pendingCenterRef.current;
|
|
1137
|
+
pendingCenterRef.current = null;
|
|
1138
|
+
center2?.();
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
fittedFrameRef.current = { frame: options.frameKey };
|
|
1142
|
+
const center = pendingCenterRef.current;
|
|
1143
|
+
pendingCenterRef.current = null;
|
|
1144
|
+
if (silent && center && !defaultViewportRef.current) {
|
|
1145
|
+
center();
|
|
1146
|
+
return true;
|
|
1147
|
+
}
|
|
1148
|
+
zoomAnimationRef.current = null;
|
|
1149
|
+
centerAnimationRef.current = null;
|
|
1035
1150
|
const seed = defaultViewportRef.current;
|
|
1036
1151
|
if (seed) {
|
|
1037
1152
|
defaultViewportRef.current = void 0;
|
|
@@ -1045,14 +1160,15 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1045
1160
|
view.y = seeded.y;
|
|
1046
1161
|
centerGuardUntilRef.current = performance.now() + DEFAULT_VIEWPORT_CENTER_GUARD_MS;
|
|
1047
1162
|
commitView(false);
|
|
1048
|
-
return;
|
|
1163
|
+
return true;
|
|
1049
1164
|
}
|
|
1050
1165
|
view.k = fitScale;
|
|
1051
1166
|
view.x = viewportRect.width / 2 - (contentOriginX + contentWidth / 2) * fitScale;
|
|
1052
1167
|
view.y = viewportRect.height / 2 - (contentOriginY + contentHeight / 2) * fitScale;
|
|
1053
1168
|
commitView(!silent);
|
|
1169
|
+
return true;
|
|
1054
1170
|
},
|
|
1055
|
-
[viewportRef, commitView]
|
|
1171
|
+
[viewportRef, commitView, options.frameKey, options.selectedNodeId]
|
|
1056
1172
|
);
|
|
1057
1173
|
const centerAnimationRef = (0, import_react.useRef)(null);
|
|
1058
1174
|
const centerAnimationFrameIdRef = (0, import_react.useRef)(null);
|
|
@@ -1086,7 +1202,19 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1086
1202
|
stepCenterAnimationRef.current = stepCenterAnimation;
|
|
1087
1203
|
}, [stepCenterAnimation]);
|
|
1088
1204
|
const centerOnElement = (0, import_react.useCallback)(
|
|
1089
|
-
(element, targetScale, obscured) => {
|
|
1205
|
+
(element, targetScale, obscured, automatic = false) => {
|
|
1206
|
+
if (automatic && restoredRevisionRef.current !== void 0 && restoredFrameRef.current === options.frameKey && restoredSelectionRef.current === options.selectedNodeId) return;
|
|
1207
|
+
if (!automatic) operationRef.current += 1;
|
|
1208
|
+
const viewportSize = viewportRef.current?.getBoundingClientRect();
|
|
1209
|
+
if (automatic && (measuredFrameRef.current !== options.frameKey || fittedFrameRef.current === null || !viewportSize?.width || !viewportSize.height)) {
|
|
1210
|
+
const operation = operationRef.current;
|
|
1211
|
+
pendingCenterRef.current = () => {
|
|
1212
|
+
if (operation !== operationRef.current) return;
|
|
1213
|
+
const currentElement = element?.id ? viewportRef.current?.querySelector(`#${CSS.escape(element.id)}`) : element;
|
|
1214
|
+
centerLatestRef.current(currentElement, targetScale, obscured ? resolveInfoPanelObscuredArea(currentElement, viewportRef.current, INFO_PANEL_WIDTH) : obscured, true);
|
|
1215
|
+
};
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1090
1218
|
const viewport = viewportRef.current;
|
|
1091
1219
|
if (!viewport || !element || performance.now() < centerGuardUntilRef.current) return;
|
|
1092
1220
|
const viewportRect = viewport.getBoundingClientRect();
|
|
@@ -1097,9 +1225,10 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1097
1225
|
const newScale = clampZoomScale(Math.max(targetScale, currentScale), minScaleRef.current);
|
|
1098
1226
|
const centerX = (rect.left + rect.width / 2 - viewportRect.left - view.x) / currentScale;
|
|
1099
1227
|
const centerY = (rect.top + rect.height / 2 - viewportRect.top - view.y) / currentScale;
|
|
1100
|
-
const availableCenterX =
|
|
1228
|
+
const availableCenterX = obscured?.side === "left" ? obscured.width + (viewportRect.width - obscured.width) / 2 : obscured?.side === "right" ? (viewportRect.width - obscured.width) / 2 : viewportRect.width / 2;
|
|
1229
|
+
const availableCenterY = obscured?.side === "bottom" ? (viewportRect.height - obscured.height) / 2 : viewportRect.height / 2;
|
|
1101
1230
|
const targetPanX = availableCenterX - centerX * newScale;
|
|
1102
|
-
const targetPanY =
|
|
1231
|
+
const targetPanY = availableCenterY - centerY * newScale;
|
|
1103
1232
|
zoomAnimationRef.current = null;
|
|
1104
1233
|
if (animationFrameIdRef.current !== null) {
|
|
1105
1234
|
cancelAnimationFrame(animationFrameIdRef.current);
|
|
@@ -1118,8 +1247,17 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1118
1247
|
centerAnimationFrameIdRef.current = requestAnimationFrame(() => stepCenterAnimationRef.current());
|
|
1119
1248
|
}
|
|
1120
1249
|
},
|
|
1121
|
-
[viewportRef]
|
|
1250
|
+
[viewportRef, options.frameKey, options.selectedNodeId]
|
|
1122
1251
|
);
|
|
1252
|
+
const centerLatestRef = (0, import_react.useRef)(centerOnElement);
|
|
1253
|
+
(0, import_react.useEffect)(() => {
|
|
1254
|
+
centerLatestRef.current = centerOnElement;
|
|
1255
|
+
}, [centerOnElement]);
|
|
1256
|
+
(0, import_react.useEffect)(() => {
|
|
1257
|
+
retryFrameRef.current = () => {
|
|
1258
|
+
fitToFrame(frameElementsRef.current, { silent: true });
|
|
1259
|
+
};
|
|
1260
|
+
}, [fitToFrame]);
|
|
1123
1261
|
const lastPointRef = (0, import_react.useRef)({ x: 0, y: 0 });
|
|
1124
1262
|
const minScaleRef = (0, import_react.useRef)(minScale);
|
|
1125
1263
|
const suppressedPointersRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
@@ -1194,6 +1332,7 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1194
1332
|
}, [handlePointerUp]);
|
|
1195
1333
|
const handlePointerDown = (0, import_react.useCallback)((event) => {
|
|
1196
1334
|
if (event.button !== 0) return;
|
|
1335
|
+
operationRef.current += 1;
|
|
1197
1336
|
const isInteractive = event.target.closest("g.node, foreignObject, .gtv-zoom-controls, .gtv-wheel-chip, .gtv-info-panel") !== null;
|
|
1198
1337
|
const isPageScrollTouch = wheelZoom === "modifier" && event.pointerType === "touch";
|
|
1199
1338
|
const pointers = activePointersRef.current;
|
|
@@ -1212,6 +1351,42 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1212
1351
|
window.addEventListener("pointercancel", stablePointerUp);
|
|
1213
1352
|
}
|
|
1214
1353
|
}, [stablePointerMove, stablePointerUp, wheelZoom]);
|
|
1354
|
+
const restore = options.viewportRestore;
|
|
1355
|
+
const restoreRevision = restore?.revision;
|
|
1356
|
+
const restoreX = restore?.viewport.x;
|
|
1357
|
+
const restoreY = restore?.viewport.y;
|
|
1358
|
+
const restoreZoom = restore?.viewport.zoom;
|
|
1359
|
+
(0, import_react.useEffect)(() => {
|
|
1360
|
+
if (restoreRevision === restoredRevisionRef.current || restoreRevision === void 0 || restoreX === void 0 || restoreY === void 0 || restoreZoom === void 0) return;
|
|
1361
|
+
if (!Number.isFinite(restoreX) || !Number.isFinite(restoreY) || !Number.isFinite(restoreZoom) || restoreZoom <= 0) return;
|
|
1362
|
+
const viewportElement = viewportRef.current;
|
|
1363
|
+
if (!viewportElement) return;
|
|
1364
|
+
restoredRevisionRef.current = restoreRevision;
|
|
1365
|
+
const operation = ++operationRef.current;
|
|
1366
|
+
zoomAnimationRef.current = null;
|
|
1367
|
+
centerAnimationRef.current = null;
|
|
1368
|
+
if (animationFrameIdRef.current !== null) cancelAnimationFrame(animationFrameIdRef.current);
|
|
1369
|
+
if (centerAnimationFrameIdRef.current !== null) cancelAnimationFrame(centerAnimationFrameIdRef.current);
|
|
1370
|
+
if (frameIdRef.current !== null) cancelAnimationFrame(frameIdRef.current);
|
|
1371
|
+
if (settleTimerRef.current !== null) clearTimeout(settleTimerRef.current);
|
|
1372
|
+
animationFrameIdRef.current = null;
|
|
1373
|
+
centerAnimationFrameIdRef.current = null;
|
|
1374
|
+
frameIdRef.current = null;
|
|
1375
|
+
settleTimerRef.current = null;
|
|
1376
|
+
activePointersRef.current.clear();
|
|
1377
|
+
suppressedPointersRef.current.clear();
|
|
1378
|
+
pinchStartRef.current = null;
|
|
1379
|
+
window.removeEventListener("pointermove", stablePointerMove);
|
|
1380
|
+
window.removeEventListener("pointerup", stablePointerUp);
|
|
1381
|
+
window.removeEventListener("pointercancel", stablePointerUp);
|
|
1382
|
+
pendingRestoreRef.current = {
|
|
1383
|
+
viewport: { x: restoreX, y: restoreY, zoom: restoreZoom },
|
|
1384
|
+
revision: restoreRevision,
|
|
1385
|
+
frame: options.frameKey,
|
|
1386
|
+
operation
|
|
1387
|
+
};
|
|
1388
|
+
if (measuredFrameRef.current === options.frameKey) retryFrameRef.current();
|
|
1389
|
+
}, [restoreRevision, restoreX, restoreY, restoreZoom, options.frameKey, options.selectedNodeId, viewportRef, commitView, stablePointerMove, stablePointerUp]);
|
|
1215
1390
|
(0, import_react.useEffect)(() => {
|
|
1216
1391
|
const pointers = activePointersRef.current;
|
|
1217
1392
|
const suppressed = suppressedPointersRef.current;
|
|
@@ -1246,17 +1421,6 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1246
1421
|
|
|
1247
1422
|
// src/use-node-info-panel.ts
|
|
1248
1423
|
var import_react2 = require("react");
|
|
1249
|
-
|
|
1250
|
-
// src/info-panel-geometry.ts
|
|
1251
|
-
function resolveInfoPanelSide() {
|
|
1252
|
-
return "left";
|
|
1253
|
-
}
|
|
1254
|
-
function resolveInfoPanelObscuredArea(element, viewport, panelWidth) {
|
|
1255
|
-
if (!element || !viewport) return null;
|
|
1256
|
-
return { width: panelWidth, side: resolveInfoPanelSide() };
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
// src/use-node-info-panel.ts
|
|
1260
1424
|
function useNodeInfoPanel({
|
|
1261
1425
|
selectedNodeId,
|
|
1262
1426
|
onSelectedNodeChange
|
|
@@ -1265,7 +1429,7 @@ function useNodeInfoPanel({
|
|
|
1265
1429
|
const [prevSelectedNodeId, setPrevSelectedNodeId] = (0, import_react2.useState)(selectedNodeId);
|
|
1266
1430
|
if (selectedNodeId !== prevSelectedNodeId) {
|
|
1267
1431
|
setPrevSelectedNodeId(selectedNodeId);
|
|
1268
|
-
if (prevSelectedNodeId
|
|
1432
|
+
if (prevSelectedNodeId !== void 0) setPanel(null);
|
|
1269
1433
|
}
|
|
1270
1434
|
const shownNodeIdRef = (0, import_react2.useRef)(null);
|
|
1271
1435
|
const onSelectedNodeChangeRef = (0, import_react2.useRef)(onSelectedNodeChange);
|
|
@@ -1275,20 +1439,32 @@ function useNodeInfoPanel({
|
|
|
1275
1439
|
});
|
|
1276
1440
|
const showNodeInfo = (0, import_react2.useCallback)(
|
|
1277
1441
|
(node, element, viewport, { notify = true } = {}) => {
|
|
1442
|
+
if (notify && selectedNodeId !== void 0) {
|
|
1443
|
+
if (node.id !== selectedNodeId) onSelectedNodeChangeRef.current?.(node);
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1278
1446
|
if (!element || !viewport) return;
|
|
1279
1447
|
setPanel({ node, side: resolveInfoPanelSide() });
|
|
1280
1448
|
const changed = node.id !== shownNodeIdRef.current;
|
|
1281
1449
|
shownNodeIdRef.current = node.id;
|
|
1282
1450
|
if (changed && notify) onSelectedNodeChangeRef.current?.(node);
|
|
1283
1451
|
},
|
|
1284
|
-
[]
|
|
1452
|
+
[selectedNodeId]
|
|
1285
1453
|
);
|
|
1286
1454
|
const closeNodeInfo = (0, import_react2.useCallback)(() => {
|
|
1455
|
+
if (selectedNodeId !== void 0) {
|
|
1456
|
+
if (selectedNodeId !== null) onSelectedNodeChangeRef.current?.(null);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1287
1459
|
setPanel(null);
|
|
1288
1460
|
shownNodeIdRef.current = null;
|
|
1289
1461
|
onSelectedNodeChangeRef.current?.(null);
|
|
1290
|
-
}, []);
|
|
1291
|
-
return {
|
|
1462
|
+
}, [selectedNodeId]);
|
|
1463
|
+
return {
|
|
1464
|
+
panel: selectedNodeId !== void 0 && panel?.node.id !== selectedNodeId ? null : panel,
|
|
1465
|
+
showNodeInfo,
|
|
1466
|
+
closeNodeInfo
|
|
1467
|
+
};
|
|
1292
1468
|
}
|
|
1293
1469
|
|
|
1294
1470
|
// src/use-tree-index.ts
|
|
@@ -1335,10 +1511,10 @@ function useTreeIndex(nodes) {
|
|
|
1335
1511
|
|
|
1336
1512
|
// src/InfoPanel.tsx
|
|
1337
1513
|
var import_react6 = require("react");
|
|
1338
|
-
var
|
|
1514
|
+
var import_md4 = require("react-icons/md");
|
|
1339
1515
|
|
|
1340
1516
|
// src/InfoPanelExpandButton.tsx
|
|
1341
|
-
var
|
|
1517
|
+
var import_md = require("react-icons/md");
|
|
1342
1518
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1343
1519
|
function InfoPanelExpandButton({ expanded, onExpandedChange, style }) {
|
|
1344
1520
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
@@ -1349,14 +1525,14 @@ function InfoPanelExpandButton({ expanded, onExpandedChange, style }) {
|
|
|
1349
1525
|
style,
|
|
1350
1526
|
onClick: () => onExpandedChange(!expanded),
|
|
1351
1527
|
"aria-label": expanded ? "Collapse panel" : "Expand panel",
|
|
1352
|
-
children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1528
|
+
children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_md.MdCloseFullscreen, { className: "gtv-icon", size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_md.MdOpenInFull, { className: "gtv-icon", size: 16 })
|
|
1353
1529
|
}
|
|
1354
1530
|
);
|
|
1355
1531
|
}
|
|
1356
1532
|
|
|
1357
1533
|
// src/InfoPanelSection.tsx
|
|
1358
1534
|
var import_react4 = require("react");
|
|
1359
|
-
var
|
|
1535
|
+
var import_md2 = require("react-icons/md");
|
|
1360
1536
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1361
1537
|
function InfoPanelSection({ title, children }) {
|
|
1362
1538
|
const [open, setOpen] = (0, import_react4.useState)(true);
|
|
@@ -1370,7 +1546,7 @@ function InfoPanelSection({ title, children }) {
|
|
|
1370
1546
|
onClick: () => setOpen((isOpen) => !isOpen),
|
|
1371
1547
|
children: [
|
|
1372
1548
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "gtv-info-panel-children-title", children: title }),
|
|
1373
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1549
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_md2.MdExpandMore, { className: "gtv-icon gtv-info-panel-section-chevron", size: 16 })
|
|
1374
1550
|
]
|
|
1375
1551
|
}
|
|
1376
1552
|
),
|
|
@@ -1380,7 +1556,7 @@ function InfoPanelSection({ title, children }) {
|
|
|
1380
1556
|
|
|
1381
1557
|
// src/NodeToolbar.tsx
|
|
1382
1558
|
var import_react5 = require("react");
|
|
1383
|
-
var
|
|
1559
|
+
var import_md3 = require("react-icons/md");
|
|
1384
1560
|
var import_fa = require("react-icons/fa");
|
|
1385
1561
|
var import_pi2 = require("react-icons/pi");
|
|
1386
1562
|
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
@@ -1458,7 +1634,7 @@ function NodeToolbar({
|
|
|
1458
1634
|
"aria-haspopup": "menu",
|
|
1459
1635
|
"aria-expanded": menuOpen,
|
|
1460
1636
|
onClick: () => setMenuOpen((open) => !open),
|
|
1461
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1637
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_md3.MdMoreVert, { size: "0.6em" })
|
|
1462
1638
|
}
|
|
1463
1639
|
),
|
|
1464
1640
|
menuOpen && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "gtv-menu-card gtv-toolbar-overflow-menu", children: [
|
|
@@ -1472,7 +1648,7 @@ function NodeToolbar({
|
|
|
1472
1648
|
onRenameRequest?.(node);
|
|
1473
1649
|
},
|
|
1474
1650
|
children: [
|
|
1475
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "gtv-menu-row-icon", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1651
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "gtv-menu-row-icon", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_md3.MdModeEdit, { className: "gtv-icon", size: 13 }) }),
|
|
1476
1652
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "gtv-menu-row-label", children: "Rename" })
|
|
1477
1653
|
]
|
|
1478
1654
|
}
|
|
@@ -1574,6 +1750,27 @@ function InfoPanel({
|
|
|
1574
1750
|
hideExpandButton = false
|
|
1575
1751
|
}) {
|
|
1576
1752
|
const panelRef = (0, import_react6.useRef)(null);
|
|
1753
|
+
const [sheetFull, setSheetFull] = (0, import_react6.useState)(false);
|
|
1754
|
+
const grabberDragRef = (0, import_react6.useRef)(null);
|
|
1755
|
+
const onGrabberPointerDown = (event) => {
|
|
1756
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
1757
|
+
grabberDragRef.current = { startY: event.clientY, dragged: false };
|
|
1758
|
+
};
|
|
1759
|
+
const onGrabberPointerUp = (event) => {
|
|
1760
|
+
const drag = grabberDragRef.current;
|
|
1761
|
+
if (!drag) return;
|
|
1762
|
+
const dy = event.clientY - drag.startY;
|
|
1763
|
+
if (Math.abs(dy) < INFO_PANEL_SHEET_DRAG_THRESHOLD_PX) return;
|
|
1764
|
+
drag.dragged = true;
|
|
1765
|
+
if (dy < 0) setSheetFull(true);
|
|
1766
|
+
else if (sheetFull) setSheetFull(false);
|
|
1767
|
+
else if (!hideClose) onClose();
|
|
1768
|
+
};
|
|
1769
|
+
const onGrabberClick = () => {
|
|
1770
|
+
const dragged = grabberDragRef.current?.dragged ?? false;
|
|
1771
|
+
grabberDragRef.current = null;
|
|
1772
|
+
if (!dragged) setSheetFull((full) => !full);
|
|
1773
|
+
};
|
|
1577
1774
|
(0, import_react6.useEffect)(() => {
|
|
1578
1775
|
panelRef.current?.toggleAttribute("inert", collapsed);
|
|
1579
1776
|
}, [collapsed]);
|
|
@@ -1676,9 +1873,21 @@ function InfoPanel({
|
|
|
1676
1873
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1677
1874
|
"div",
|
|
1678
1875
|
{
|
|
1679
|
-
className: `gtv-info-panel gtv-info-panel--${side}${expanded ? " gtv-info-panel--expanded" : ""}${expanded && !hasAside ? " gtv-info-panel--expanded-solo" : ""}${collapsed ? " gtv-info-panel--collapsed" : ""}`,
|
|
1876
|
+
className: `gtv-info-panel gtv-info-panel--sheet${sheetFull ? " gtv-info-panel--sheet-full" : ""} gtv-info-panel--${side}${expanded ? " gtv-info-panel--expanded" : ""}${expanded && !hasAside ? " gtv-info-panel--expanded-solo" : ""}${collapsed ? " gtv-info-panel--collapsed" : ""}`,
|
|
1680
1877
|
ref: panelRef,
|
|
1681
1878
|
children: [
|
|
1879
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1880
|
+
"button",
|
|
1881
|
+
{
|
|
1882
|
+
type: "button",
|
|
1883
|
+
className: "gtv-info-panel-grabber",
|
|
1884
|
+
"aria-label": sheetFull ? "Lower panel" : "Raise panel",
|
|
1885
|
+
"aria-expanded": sheetFull,
|
|
1886
|
+
onPointerDown: onGrabberPointerDown,
|
|
1887
|
+
onPointerUp: onGrabberPointerUp,
|
|
1888
|
+
onClick: onGrabberClick
|
|
1889
|
+
}
|
|
1890
|
+
),
|
|
1682
1891
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1683
1892
|
"div",
|
|
1684
1893
|
{
|
|
@@ -1694,7 +1903,7 @@ function InfoPanel({
|
|
|
1694
1903
|
onClick: onPrevious,
|
|
1695
1904
|
"aria-label": previousLabel ? `Previous: ${previousLabel}` : "Previous",
|
|
1696
1905
|
title: previousLabel,
|
|
1697
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1906
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_md4.MdArrowBack, { className: "gtv-icon", size: 16 })
|
|
1698
1907
|
}
|
|
1699
1908
|
),
|
|
1700
1909
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
@@ -1717,7 +1926,7 @@ function InfoPanel({
|
|
|
1717
1926
|
onClick: onZoomIn,
|
|
1718
1927
|
"aria-label": "Zoom in",
|
|
1719
1928
|
title: "Zoom in",
|
|
1720
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1929
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_md4.MdZoomIn, { className: "gtv-icon", size: 16 })
|
|
1721
1930
|
}
|
|
1722
1931
|
),
|
|
1723
1932
|
onExpandedChange && !hideExpandButton && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(InfoPanelExpandButton, { expanded, onExpandedChange, style: { color: textColor } }),
|
|
@@ -1729,7 +1938,7 @@ function InfoPanel({
|
|
|
1729
1938
|
style: { color: textColor },
|
|
1730
1939
|
onClick: onClose,
|
|
1731
1940
|
"aria-label": "Close",
|
|
1732
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1941
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_md4.MdClose, { className: "gtv-icon", size: 16 })
|
|
1733
1942
|
}
|
|
1734
1943
|
)
|
|
1735
1944
|
]
|
|
@@ -1753,7 +1962,7 @@ function InfoPanel({
|
|
|
1753
1962
|
|
|
1754
1963
|
// src/OverlayPanel.tsx
|
|
1755
1964
|
var import_react7 = require("react");
|
|
1756
|
-
var
|
|
1965
|
+
var import_md5 = require("react-icons/md");
|
|
1757
1966
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1758
1967
|
function OverlayPanel({
|
|
1759
1968
|
title,
|
|
@@ -1793,7 +2002,7 @@ function OverlayPanel({
|
|
|
1793
2002
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "gtv-info-panel-header", children: [
|
|
1794
2003
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: `gtv-info-panel-title${hideTitle ? " gtv-info-panel-title--hidden" : ""}`, children: title }),
|
|
1795
2004
|
onExpandedChange && !hideExpandButton && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(InfoPanelExpandButton, { expanded, onExpandedChange }),
|
|
1796
|
-
!hideClose && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: "gtv-info-panel-close", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
2005
|
+
!hideClose && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: "gtv-info-panel-close", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_md5.MdClose, { className: "gtv-icon", size: 16 }) })
|
|
1797
2006
|
] }),
|
|
1798
2007
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "gtv-info-panel-body", children: [
|
|
1799
2008
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "gtv-info-panel-aside gtv-overlay-content", children: content }),
|
|
@@ -1830,6 +2039,8 @@ function GenreTree({
|
|
|
1830
2039
|
hideInfoPanelClose = false,
|
|
1831
2040
|
hideInfoPanelTitle = false,
|
|
1832
2041
|
itemCountLabel,
|
|
2042
|
+
itemCountScaleMax,
|
|
2043
|
+
nodeCountForSizing,
|
|
1833
2044
|
readOnly,
|
|
1834
2045
|
renderDangerZone,
|
|
1835
2046
|
expanded,
|
|
@@ -1839,8 +2050,10 @@ function GenreTree({
|
|
|
1839
2050
|
overlayPanel,
|
|
1840
2051
|
wheelZoom,
|
|
1841
2052
|
defaultViewport,
|
|
2053
|
+
viewportRestore,
|
|
1842
2054
|
onViewportChange,
|
|
1843
2055
|
controlsOrientation = "vertical",
|
|
2056
|
+
showZoomControls = "auto",
|
|
1844
2057
|
floatingControlsLeading,
|
|
1845
2058
|
busy
|
|
1846
2059
|
}) {
|
|
@@ -1848,8 +2061,13 @@ function GenreTree({
|
|
|
1848
2061
|
const svgRef = (0, import_react8.useRef)(null);
|
|
1849
2062
|
const viewportRef = (0, import_react8.useRef)(null);
|
|
1850
2063
|
const stageRef = (0, import_react8.useRef)(null);
|
|
1851
|
-
const
|
|
2064
|
+
const viewportFrame = (0, import_react8.useMemo)(() => JSON.stringify([orientation, nodes.map(({ id, parentId, side }) => [id, parentId, side])]), [nodes, orientation]);
|
|
2065
|
+
const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, viewportRestore, selectedNodeId: selectedNodeIdProp, onViewportChange, frameKey: viewportFrame });
|
|
1852
2066
|
const { panel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId: selectedNodeIdProp, onSelectedNodeChange });
|
|
2067
|
+
const itemCountRange = (0, import_react8.useMemo)(
|
|
2068
|
+
() => getItemCountRange(nodes, { itemCountScaleMax, nodeCountForSizing: nodeCountForSizing ?? nodes.length }),
|
|
2069
|
+
[nodes, itemCountScaleMax, nodeCountForSizing]
|
|
2070
|
+
);
|
|
1853
2071
|
const showNodeInfoRef = (0, import_react8.useRef)(showNodeInfo);
|
|
1854
2072
|
(0, import_react8.useEffect)(() => {
|
|
1855
2073
|
showNodeInfoRef.current = showNodeInfo;
|
|
@@ -1864,7 +2082,8 @@ function GenreTree({
|
|
|
1864
2082
|
d3,
|
|
1865
2083
|
root,
|
|
1866
2084
|
orientation,
|
|
1867
|
-
depthSpacingScale
|
|
2085
|
+
depthSpacingScale,
|
|
2086
|
+
itemCountRange
|
|
1868
2087
|
);
|
|
1869
2088
|
const {
|
|
1870
2089
|
svgWidth: width,
|
|
@@ -1876,7 +2095,8 @@ function GenreTree({
|
|
|
1876
2095
|
originalTreeData,
|
|
1877
2096
|
orientation,
|
|
1878
2097
|
hideRoot,
|
|
1879
|
-
depthSpacingScale
|
|
2098
|
+
depthSpacingScale,
|
|
2099
|
+
itemCountRange
|
|
1880
2100
|
);
|
|
1881
2101
|
const reshapedTreeData = setupTreeLayout(
|
|
1882
2102
|
d3,
|
|
@@ -1885,7 +2105,8 @@ function GenreTree({
|
|
|
1885
2105
|
orientation,
|
|
1886
2106
|
rootDepthOffset,
|
|
1887
2107
|
width,
|
|
1888
|
-
depthSpacingScale
|
|
2108
|
+
depthSpacingScale,
|
|
2109
|
+
itemCountRange
|
|
1889
2110
|
);
|
|
1890
2111
|
return {
|
|
1891
2112
|
treeData: reshapedTreeData,
|
|
@@ -1893,7 +2114,7 @@ function GenreTree({
|
|
|
1893
2114
|
svgWidth: width,
|
|
1894
2115
|
svgHeight: height
|
|
1895
2116
|
};
|
|
1896
|
-
}, [nodes, rootColor, orientation, hideRoot, depthSpacingScale]);
|
|
2117
|
+
}, [nodes, rootColor, orientation, hideRoot, depthSpacingScale, itemCountRange]);
|
|
1897
2118
|
const reparentForbiddenIds = (0, import_react8.useMemo)(() => {
|
|
1898
2119
|
if (!reparentingNodeId) return [];
|
|
1899
2120
|
const reparentingHierarchyNode = treeData.descendants().find((d) => d.data.id === reparentingNodeId);
|
|
@@ -1944,7 +2165,8 @@ function GenreTree({
|
|
|
1944
2165
|
},
|
|
1945
2166
|
orientation,
|
|
1946
2167
|
hideRoot,
|
|
1947
|
-
effectiveSelectedNodeId
|
|
2168
|
+
effectiveSelectedNodeId,
|
|
2169
|
+
itemCountRange
|
|
1948
2170
|
);
|
|
1949
2171
|
}, [
|
|
1950
2172
|
treeData,
|
|
@@ -1965,7 +2187,8 @@ function GenreTree({
|
|
|
1965
2187
|
additionalActions,
|
|
1966
2188
|
orientation,
|
|
1967
2189
|
hideRoot,
|
|
1968
|
-
effectiveSelectedNodeId
|
|
2190
|
+
effectiveSelectedNodeId,
|
|
2191
|
+
itemCountRange
|
|
1969
2192
|
]);
|
|
1970
2193
|
(0, import_react8.useEffect)(() => {
|
|
1971
2194
|
if (!interactive || !selectedNodeIdProp) return;
|
|
@@ -1978,7 +2201,8 @@ function GenreTree({
|
|
|
1978
2201
|
centerOnElementRef.current(
|
|
1979
2202
|
element,
|
|
1980
2203
|
ZOOM_FOCUS_SCALE,
|
|
1981
|
-
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH)
|
|
2204
|
+
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),
|
|
2205
|
+
true
|
|
1982
2206
|
);
|
|
1983
2207
|
showNodeInfoRef.current(targetNode, element, viewportRef.current, { notify: false });
|
|
1984
2208
|
}, [interactive, selectedNodeIdProp, index, panel]);
|
|
@@ -1990,12 +2214,10 @@ function GenreTree({
|
|
|
1990
2214
|
}
|
|
1991
2215
|
};
|
|
1992
2216
|
}, []);
|
|
1993
|
-
const hasInitialFitRef = (0, import_react8.useRef)(false);
|
|
1994
2217
|
(0, import_react8.useEffect)(() => {
|
|
1995
|
-
if (
|
|
2218
|
+
if (!interactive) return;
|
|
1996
2219
|
const elements = queryTreeContentElements(svgRef.current);
|
|
1997
2220
|
if (elements.length === 0) return;
|
|
1998
|
-
hasInitialFitRef.current = true;
|
|
1999
2221
|
panZoom.fitToFrame(elements, { silent: true });
|
|
2000
2222
|
});
|
|
2001
2223
|
const svg = /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
@@ -2126,6 +2348,7 @@ function GenreTree({
|
|
|
2126
2348
|
{
|
|
2127
2349
|
panZoom,
|
|
2128
2350
|
orientation: controlsOrientation,
|
|
2351
|
+
zoomControls: showZoomControls,
|
|
2129
2352
|
leading: floatingControlsLeading,
|
|
2130
2353
|
busy,
|
|
2131
2354
|
onFit: () => panZoom.fitToFrame(queryTreeContentElements(svgRef.current))
|
|
@@ -2238,6 +2461,8 @@ function WheelCore({
|
|
|
2238
2461
|
hideInfoPanelClose = false,
|
|
2239
2462
|
hideInfoPanelTitle = false,
|
|
2240
2463
|
itemCountLabel,
|
|
2464
|
+
itemCountScaleMax,
|
|
2465
|
+
nodeCountForSizing,
|
|
2241
2466
|
readOnly,
|
|
2242
2467
|
renderDangerZone,
|
|
2243
2468
|
expanded,
|
|
@@ -2247,12 +2472,19 @@ function WheelCore({
|
|
|
2247
2472
|
overlayPanel,
|
|
2248
2473
|
wheelZoom,
|
|
2249
2474
|
defaultViewport,
|
|
2475
|
+
viewportRestore,
|
|
2250
2476
|
onViewportChange,
|
|
2251
2477
|
controlsOrientation = "vertical",
|
|
2478
|
+
showZoomControls = "auto",
|
|
2252
2479
|
floatingControlsLeading,
|
|
2253
2480
|
busy,
|
|
2254
2481
|
selectedNodeId
|
|
2255
2482
|
}) {
|
|
2483
|
+
const fullNodeCountForSizing = nodeCountForSizing ?? nodes.length;
|
|
2484
|
+
const wheelSizeRange = (0, import_react9.useMemo)(
|
|
2485
|
+
() => getItemCountRange(nodes, { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }),
|
|
2486
|
+
[nodes, itemCountScaleMax, fullNodeCountForSizing]
|
|
2487
|
+
);
|
|
2256
2488
|
const treeOrientation = direction === "left" ? "horizontal-anchored" : "vertical";
|
|
2257
2489
|
const landingAngle = direction === "left" ? 90 : 0;
|
|
2258
2490
|
const chipHalfExtentVar = direction === "left" ? "--gtv-wheel-chip-half-width" : "--gtv-wheel-chip-half-height";
|
|
@@ -2262,10 +2494,10 @@ function WheelCore({
|
|
|
2262
2494
|
return getGenreTreeColor(nodes.find((n) => n.id === rootId)?.wikidataId);
|
|
2263
2495
|
};
|
|
2264
2496
|
const wheelRadius = (0, import_react9.useMemo)(
|
|
2265
|
-
() => calculateWheelRadius(groups.length, MAX_NODE_WIDTH, WHEEL_RADIUS),
|
|
2266
|
-
[groups.length]
|
|
2497
|
+
() => calculateWheelRadius(groups.length, wheelSizeRange.maxNodeWidth ?? MAX_NODE_WIDTH, WHEEL_RADIUS),
|
|
2498
|
+
[groups.length, wheelSizeRange.maxNodeWidth]
|
|
2267
2499
|
);
|
|
2268
|
-
const wheelViewportHeight = wheelRadius * 2 + MAX_NODE_HEIGHT / 2;
|
|
2500
|
+
const wheelViewportHeight = wheelRadius * 2 + (wheelSizeRange.maxNodeHeight ?? MAX_NODE_HEIGHT) / 2;
|
|
2269
2501
|
const rootItemCountById = (0, import_react9.useMemo)(
|
|
2270
2502
|
() => new Map(groups.map((group) => [group.root.id, group.root.itemCount])),
|
|
2271
2503
|
[groups]
|
|
@@ -2282,7 +2514,8 @@ function WheelCore({
|
|
|
2282
2514
|
);
|
|
2283
2515
|
const viewportRef = (0, import_react9.useRef)(null);
|
|
2284
2516
|
const stageRef = (0, import_react9.useRef)(null);
|
|
2285
|
-
const
|
|
2517
|
+
const viewportFrame = (0, import_react9.useMemo)(() => JSON.stringify(nodes.map(({ id, parentId, side }) => [id, parentId, side])), [nodes]);
|
|
2518
|
+
const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, viewportRestore, selectedNodeId, onViewportChange, frameKey: viewportFrame });
|
|
2286
2519
|
const { panel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId, onSelectedNodeChange });
|
|
2287
2520
|
const wheelCircleRef = (0, import_react9.useRef)(null);
|
|
2288
2521
|
const treeAnchorRef = (0, import_react9.useRef)(null);
|
|
@@ -2296,15 +2529,12 @@ function WheelCore({
|
|
|
2296
2529
|
(0, import_react9.useEffect)(() => {
|
|
2297
2530
|
if (effectiveRootId) onRootSelectRef.current?.(effectiveRootId);
|
|
2298
2531
|
}, [effectiveRootId]);
|
|
2299
|
-
const hasInitialFitRef = (0, import_react9.useRef)(false);
|
|
2300
2532
|
(0, import_react9.useEffect)(() => {
|
|
2301
|
-
if (hasInitialFitRef.current) return;
|
|
2302
2533
|
const elements = [
|
|
2303
2534
|
wheelCircleRef.current,
|
|
2304
2535
|
...queryTreeContentElements(treeAnchorRef.current)
|
|
2305
2536
|
];
|
|
2306
2537
|
if (!elements.some(Boolean)) return;
|
|
2307
|
-
hasInitialFitRef.current = true;
|
|
2308
2538
|
panZoom.fitToFrame(elements, { silent: true });
|
|
2309
2539
|
});
|
|
2310
2540
|
const selectedGroup = groups.find((group) => group.root.id === effectiveRootId) ?? null;
|
|
@@ -2312,16 +2542,18 @@ function WheelCore({
|
|
|
2312
2542
|
() => getItemCountRange(
|
|
2313
2543
|
groups.map((group) => ({
|
|
2314
2544
|
itemCount: rootItemCountById.get(group.root.id)
|
|
2315
|
-
}))
|
|
2545
|
+
})),
|
|
2546
|
+
{ itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }
|
|
2316
2547
|
),
|
|
2317
|
-
[groups, rootItemCountById]
|
|
2548
|
+
[groups, rootItemCountById, itemCountScaleMax, fullNodeCountForSizing]
|
|
2318
2549
|
);
|
|
2319
2550
|
const rootChipHalfExtent = selectedGroup ? calculateRootAnchorClearance(
|
|
2320
2551
|
d32,
|
|
2321
2552
|
selectedGroup.nodes,
|
|
2322
2553
|
rootItemCountById.get(selectedGroup.root.id),
|
|
2323
2554
|
rootItemCountRange,
|
|
2324
|
-
direction === "left" ? "WIDTH" : "HEIGHT"
|
|
2555
|
+
direction === "left" ? "WIDTH" : "HEIGHT",
|
|
2556
|
+
{ itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }
|
|
2325
2557
|
) : 0;
|
|
2326
2558
|
const sectorFillGradient = (0, import_react9.useMemo)(
|
|
2327
2559
|
() => buildWheelSectorGradient(
|
|
@@ -2362,7 +2594,8 @@ function WheelCore({
|
|
|
2362
2594
|
panZoom.centerOnElement(
|
|
2363
2595
|
element,
|
|
2364
2596
|
ZOOM_FOCUS_SCALE,
|
|
2365
|
-
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH)
|
|
2597
|
+
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),
|
|
2598
|
+
true
|
|
2366
2599
|
);
|
|
2367
2600
|
showNodeInfo(targetNode, element, viewportRef.current, { notify: false });
|
|
2368
2601
|
}, [selectedNodeId, effectiveRootId, nodes, panel, showNodeInfo]);
|
|
@@ -2411,6 +2644,8 @@ function WheelCore({
|
|
|
2411
2644
|
{
|
|
2412
2645
|
nodes: selectedGroup.nodes,
|
|
2413
2646
|
itemCountLabel,
|
|
2647
|
+
itemCountScaleMax,
|
|
2648
|
+
nodeCountForSizing: fullNodeCountForSizing,
|
|
2414
2649
|
orientation: treeOrientation,
|
|
2415
2650
|
hideRoot: true,
|
|
2416
2651
|
interactive: false,
|
|
@@ -2622,6 +2857,7 @@ function WheelCore({
|
|
|
2622
2857
|
{
|
|
2623
2858
|
panZoom,
|
|
2624
2859
|
orientation: controlsOrientation,
|
|
2860
|
+
zoomControls: showZoomControls,
|
|
2625
2861
|
leading: floatingControlsLeading,
|
|
2626
2862
|
busy,
|
|
2627
2863
|
onFit: () => panZoom.fitToFrame([
|
|
@@ -2654,18 +2890,32 @@ var d33 = __toESM(require("d3"), 1);
|
|
|
2654
2890
|
// src/defer-click.ts
|
|
2655
2891
|
function createDeferredClick() {
|
|
2656
2892
|
let timeoutId = null;
|
|
2893
|
+
let pendingKey = void 0;
|
|
2894
|
+
let doubledAt = -Infinity;
|
|
2657
2895
|
const cancel = () => {
|
|
2658
2896
|
if (timeoutId !== null) clearTimeout(timeoutId);
|
|
2659
2897
|
timeoutId = null;
|
|
2660
2898
|
};
|
|
2661
|
-
const schedule = (action) => {
|
|
2899
|
+
const schedule = (action, onDouble, key) => {
|
|
2900
|
+
if (onDouble && timeoutId !== null && key === pendingKey) {
|
|
2901
|
+
cancel();
|
|
2902
|
+
doubledAt = Date.now();
|
|
2903
|
+
onDouble();
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2662
2906
|
cancel();
|
|
2907
|
+
pendingKey = key;
|
|
2663
2908
|
timeoutId = setTimeout(() => {
|
|
2664
2909
|
timeoutId = null;
|
|
2665
2910
|
action();
|
|
2666
2911
|
}, DOUBLE_CLICK_DELAY_MS);
|
|
2667
2912
|
};
|
|
2668
|
-
|
|
2913
|
+
const doubleClick = (onDouble) => {
|
|
2914
|
+
cancel();
|
|
2915
|
+
if (Date.now() - doubledAt < DOUBLE_CLICK_DELAY_MS) return;
|
|
2916
|
+
onDouble();
|
|
2917
|
+
};
|
|
2918
|
+
return { schedule, cancel, doubleClick };
|
|
2669
2919
|
}
|
|
2670
2920
|
|
|
2671
2921
|
// src/radial-wheel-geometry.ts
|
|
@@ -2855,13 +3105,15 @@ function calculatePopSubtreeRadialExtent(hierarchy, coreRootCircleRadius = 0) {
|
|
|
2855
3105
|
POP_TREE_DEPTH_RADIAL_SPACING
|
|
2856
3106
|
) + MAX_NODE_WIDTH / 2 + POP_SUBTREE_OUTER_MARGIN;
|
|
2857
3107
|
}
|
|
2858
|
-
function computePopRadialLayout(d3Lib, hierarchy, wedgeCenterAngleDegrees, coreRootCircleRadius, wedgeSpanDegrees = POP_WEDGE_SPAN_DEGREES, depthSpacing = POP_TREE_DEPTH_RADIAL_SPACING) {
|
|
3108
|
+
function computePopRadialLayout(d3Lib, hierarchy, wedgeCenterAngleDegrees, coreRootCircleRadius, wedgeSpanDegrees = POP_WEDGE_SPAN_DEGREES, depthSpacing = POP_TREE_DEPTH_RADIAL_SPACING, dotRadius = NODE_LAYOUT_SLOT_RADIUS) {
|
|
2859
3109
|
return layoutWedge(
|
|
2860
3110
|
d3Lib,
|
|
2861
3111
|
hierarchy,
|
|
2862
3112
|
wedgeCenterAngleDegrees,
|
|
2863
3113
|
wedgeSpanDegrees,
|
|
2864
|
-
popRadiusOfDepth(coreRootCircleRadius, depthSpacing)
|
|
3114
|
+
popRadiusOfDepth(coreRootCircleRadius, depthSpacing),
|
|
3115
|
+
"center",
|
|
3116
|
+
dotRadius
|
|
2865
3117
|
);
|
|
2866
3118
|
}
|
|
2867
3119
|
var MIN_DOT_GAP = 5.6;
|
|
@@ -2871,15 +3123,15 @@ function popRadiusOfDepth(coreRootCircleRadius, depthSpacing) {
|
|
|
2871
3123
|
function coreRadiusOfDepth(coreRootCircleRadius, depthSpacing) {
|
|
2872
3124
|
return (depth) => getRadialDepthRadius(depth + 1, coreRootCircleRadius, depthSpacing);
|
|
2873
3125
|
}
|
|
2874
|
-
function measureWedge(_d3Lib, hierarchy, radiusOfDepth) {
|
|
2875
|
-
const
|
|
3126
|
+
function measureWedge(_d3Lib, hierarchy, radiusOfDepth, dotRadius = NODE_LAYOUT_SLOT_RADIUS) {
|
|
3127
|
+
const slotRadius = () => dotRadius;
|
|
2876
3128
|
const offsetFromParent = /* @__PURE__ */ new Map();
|
|
2877
3129
|
const shiftLevel = (level, by) => ({
|
|
2878
3130
|
left: { ...level.left, x: level.left.x + by },
|
|
2879
3131
|
right: { ...level.right, x: level.right.x + by }
|
|
2880
3132
|
});
|
|
2881
3133
|
const pack = (node) => {
|
|
2882
|
-
const own = { x: 0, dotRadius:
|
|
3134
|
+
const own = { x: 0, dotRadius: slotRadius() };
|
|
2883
3135
|
if (!node.children) return [{ left: own, right: own }];
|
|
2884
3136
|
const below = [];
|
|
2885
3137
|
const childOffsets = [];
|
|
@@ -2911,7 +3163,7 @@ function measureWedge(_d3Lib, hierarchy, radiusOfDepth) {
|
|
|
2911
3163
|
let min = Infinity;
|
|
2912
3164
|
let max = -Infinity;
|
|
2913
3165
|
hierarchy.each((d) => {
|
|
2914
|
-
const inset = (
|
|
3166
|
+
const inset = (slotRadius() + MIN_DOT_GAP / 2) / radiusOfDepth(d.depth);
|
|
2915
3167
|
min = Math.min(min, d.x - inset);
|
|
2916
3168
|
max = Math.max(max, d.x + inset);
|
|
2917
3169
|
});
|
|
@@ -2931,8 +3183,8 @@ function splitAtBranching(hierarchy) {
|
|
|
2931
3183
|
const children = branching.children;
|
|
2932
3184
|
return children && children.length > 1 ? { chain, branching, children } : null;
|
|
2933
3185
|
}
|
|
2934
|
-
function layoutWedge(d3Lib, hierarchy, wedgeCenterAngleDegrees, wedgeSpanDegrees, radiusOfDepth, slack = "center") {
|
|
2935
|
-
const neededRad = measureWedge(d3Lib, hierarchy, radiusOfDepth);
|
|
3186
|
+
function layoutWedge(d3Lib, hierarchy, wedgeCenterAngleDegrees, wedgeSpanDegrees, radiusOfDepth, slack = "center", dotRadius = NODE_LAYOUT_SLOT_RADIUS) {
|
|
3187
|
+
const neededRad = measureWedge(d3Lib, hierarchy, radiusOfDepth, dotRadius);
|
|
2936
3188
|
const wedgeSpanRad = wedgeSpanDegrees * Math.PI / 180;
|
|
2937
3189
|
const scale = Math.min(MAX_WEDGE_STRETCH, wedgeSpanRad / neededRad);
|
|
2938
3190
|
const wedgeCenterRad = wedgeCenterAngleDegrees * Math.PI / 180;
|
|
@@ -3161,12 +3413,12 @@ function renderPopSubtree(d3Lib, svg, hierarchy, rootColor, reparentingNodeId, r
|
|
|
3161
3413
|
group.on("click", (event) => {
|
|
3162
3414
|
if (reparentingNodeId) return;
|
|
3163
3415
|
const element = event.currentTarget;
|
|
3164
|
-
deferredClick.schedule(
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
onNodeDoubleClick?.(d.data);
|
|
3416
|
+
deferredClick.schedule(
|
|
3417
|
+
() => onNodeClick?.(d.data, event, element),
|
|
3418
|
+
() => onNodeDoubleClick?.(d.data)
|
|
3419
|
+
);
|
|
3169
3420
|
});
|
|
3421
|
+
group.on("dblclick", () => deferredClick.doubleClick(() => onNodeDoubleClick?.(d.data)));
|
|
3170
3422
|
});
|
|
3171
3423
|
}
|
|
3172
3424
|
function hideCollidingLabels(d3Lib, container, itemCountRange, zoomScale) {
|
|
@@ -3243,14 +3495,15 @@ function buildCoreHierarchy(d3Lib, coreNodes) {
|
|
|
3243
3495
|
});
|
|
3244
3496
|
return hierarchy;
|
|
3245
3497
|
}
|
|
3246
|
-
function computeCoreRadialLayout(d3Lib, hierarchy, wedgeCenterAngleDegrees, wedgeSpanDegrees, coreRootCircleRadius, depthSpacing) {
|
|
3498
|
+
function computeCoreRadialLayout(d3Lib, hierarchy, wedgeCenterAngleDegrees, wedgeSpanDegrees, coreRootCircleRadius, depthSpacing, dotRadius = NODE_LAYOUT_SLOT_RADIUS) {
|
|
3247
3499
|
return layoutWedge(
|
|
3248
3500
|
d3Lib,
|
|
3249
3501
|
hierarchy,
|
|
3250
3502
|
wedgeCenterAngleDegrees,
|
|
3251
3503
|
wedgeSpanDegrees,
|
|
3252
3504
|
coreRadiusOfDepth(coreRootCircleRadius, depthSpacing),
|
|
3253
|
-
"between-children"
|
|
3505
|
+
"between-children",
|
|
3506
|
+
dotRadius
|
|
3254
3507
|
);
|
|
3255
3508
|
}
|
|
3256
3509
|
function calculateCoreSubtreeRadialExtent(hierarchy, depthSpacing, coreRootCircleRadius = 0) {
|
|
@@ -3324,6 +3577,8 @@ function WheelRadialCore({
|
|
|
3324
3577
|
hideInfoPanelClose = false,
|
|
3325
3578
|
hideInfoPanelTitle = false,
|
|
3326
3579
|
itemCountLabel,
|
|
3580
|
+
itemCountScaleMax,
|
|
3581
|
+
nodeCountForSizing,
|
|
3327
3582
|
readOnly,
|
|
3328
3583
|
renderDangerZone,
|
|
3329
3584
|
expanded,
|
|
@@ -3333,12 +3588,15 @@ function WheelRadialCore({
|
|
|
3333
3588
|
overlayPanel,
|
|
3334
3589
|
wheelZoom,
|
|
3335
3590
|
defaultViewport,
|
|
3591
|
+
viewportRestore,
|
|
3336
3592
|
onViewportChange,
|
|
3337
3593
|
controlsOrientation = "vertical",
|
|
3594
|
+
showZoomControls = "auto",
|
|
3338
3595
|
floatingControlsLeading,
|
|
3339
3596
|
busy,
|
|
3340
3597
|
selectedNodeId
|
|
3341
3598
|
}) {
|
|
3599
|
+
const fullNodeCountForSizing = nodeCountForSizing ?? nodes.length;
|
|
3342
3600
|
const index = useTreeIndex(nodes);
|
|
3343
3601
|
const groups = (0, import_react10.useMemo)(() => groupNodesByRoot(nodes), [nodes]);
|
|
3344
3602
|
const rootColorOf = (node) => getGenreTreeColor(index.byId.get(index.rootIdById.get(node.id) ?? node.id)?.wikidataId);
|
|
@@ -3347,7 +3605,8 @@ function WheelRadialCore({
|
|
|
3347
3605
|
);
|
|
3348
3606
|
const viewportRef = (0, import_react10.useRef)(null);
|
|
3349
3607
|
const stageRef = (0, import_react10.useRef)(null);
|
|
3350
|
-
const
|
|
3608
|
+
const viewportFrame = (0, import_react10.useMemo)(() => JSON.stringify(nodes.map(({ id, parentId, side }) => [id, parentId, side])), [nodes]);
|
|
3609
|
+
const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, viewportRestore, selectedNodeId, onViewportChange, frameKey: viewportFrame });
|
|
3351
3610
|
const { panel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId, onSelectedNodeChange });
|
|
3352
3611
|
const centerOnElementRef = (0, import_react10.useRef)(panZoom.centerOnElement);
|
|
3353
3612
|
(0, import_react10.useEffect)(() => {
|
|
@@ -3391,11 +3650,15 @@ function WheelRadialCore({
|
|
|
3391
3650
|
() => getItemCountRange(
|
|
3392
3651
|
groups.map((group) => ({
|
|
3393
3652
|
itemCount: rootItemCountById.get(group.root.id)
|
|
3394
|
-
}))
|
|
3653
|
+
})),
|
|
3654
|
+
{ itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }
|
|
3395
3655
|
),
|
|
3396
|
-
[groups, rootItemCountById]
|
|
3656
|
+
[groups, rootItemCountById, itemCountScaleMax, fullNodeCountForSizing]
|
|
3657
|
+
);
|
|
3658
|
+
const wheelItemCountRange = (0, import_react10.useMemo)(
|
|
3659
|
+
() => getItemCountRange(nodes, { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }),
|
|
3660
|
+
[nodes, itemCountScaleMax, fullNodeCountForSizing]
|
|
3397
3661
|
);
|
|
3398
|
-
const wheelItemCountRange = (0, import_react10.useMemo)(() => getItemCountRange(nodes), [nodes]);
|
|
3399
3662
|
const sectorSpanByRootId = (0, import_react10.useMemo)(() => {
|
|
3400
3663
|
const map = /* @__PURE__ */ new Map();
|
|
3401
3664
|
if (groups.length <= 1) return map;
|
|
@@ -3445,10 +3708,10 @@ function WheelRadialCore({
|
|
|
3445
3708
|
const chipClearanceFloor = (0, import_react10.useMemo)(
|
|
3446
3709
|
() => calculateWheelRadiusForAngles(
|
|
3447
3710
|
layout.map((slot) => slot.angle),
|
|
3448
|
-
MAX_NODE_WIDTH,
|
|
3711
|
+
wheelItemCountRange.maxNodeWidth ?? MAX_NODE_WIDTH,
|
|
3449
3712
|
WHEEL_RADIUS
|
|
3450
3713
|
),
|
|
3451
|
-
[layout]
|
|
3714
|
+
[layout, wheelItemCountRange.maxNodeWidth]
|
|
3452
3715
|
);
|
|
3453
3716
|
const maxCoreExtentDelta = (0, import_react10.useMemo)(() => {
|
|
3454
3717
|
let extent = 0;
|
|
@@ -3562,12 +3825,9 @@ function WheelRadialCore({
|
|
|
3562
3825
|
wheelItemCountRange,
|
|
3563
3826
|
panel?.node.id
|
|
3564
3827
|
]);
|
|
3565
|
-
const hasInitialFitRef = (0, import_react10.useRef)(false);
|
|
3566
3828
|
(0, import_react10.useEffect)(() => {
|
|
3567
|
-
if (hasInitialFitRef.current) return;
|
|
3568
3829
|
const elements = [wheelCircleRef.current, coreSvgRef.current];
|
|
3569
3830
|
if (!elements.some(Boolean)) return;
|
|
3570
|
-
hasInitialFitRef.current = true;
|
|
3571
3831
|
panZoom.fitToFrame(elements, { silent: true });
|
|
3572
3832
|
});
|
|
3573
3833
|
const handleChipClick = (rootId) => {
|
|
@@ -3583,7 +3843,8 @@ function WheelRadialCore({
|
|
|
3583
3843
|
panZoom.centerOnElement(
|
|
3584
3844
|
element,
|
|
3585
3845
|
ZOOM_FOCUS_SCALE,
|
|
3586
|
-
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH)
|
|
3846
|
+
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),
|
|
3847
|
+
true
|
|
3587
3848
|
);
|
|
3588
3849
|
showNodeInfo(targetNode, element, viewportRef.current, { notify: false });
|
|
3589
3850
|
}, [selectedNodeId, index, panel, showNodeInfo]);
|
|
@@ -3818,6 +4079,7 @@ function WheelRadialCore({
|
|
|
3818
4079
|
{
|
|
3819
4080
|
panZoom,
|
|
3820
4081
|
orientation: controlsOrientation,
|
|
4082
|
+
zoomControls: showZoomControls,
|
|
3821
4083
|
leading: floatingControlsLeading,
|
|
3822
4084
|
busy,
|
|
3823
4085
|
onFit: () => panZoom.fitToFrame([wheelCircleRef.current, coreSvgRef.current])
|
|
@@ -3864,7 +4126,7 @@ function useFocusHistory(focusNodeId, isFocusable) {
|
|
|
3864
4126
|
|
|
3865
4127
|
// src/WheelBreadcrumb.tsx
|
|
3866
4128
|
var import_react12 = require("react");
|
|
3867
|
-
var
|
|
4129
|
+
var import_md6 = require("react-icons/md");
|
|
3868
4130
|
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
3869
4131
|
var MAX_VISIBLE_ANCESTORS = 3;
|
|
3870
4132
|
var ANCESTORS_KEPT_WHEN_COLLAPSED = 2;
|
|
@@ -3882,7 +4144,7 @@ function WheelBreadcrumb({ ancestors, currentName, onHome, onAncestor }) {
|
|
|
3882
4144
|
"aria-label": "All genres",
|
|
3883
4145
|
title: "All genres",
|
|
3884
4146
|
onClick: onHome,
|
|
3885
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
4147
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_md6.MdHome, { className: "gtv-icon", size: 14 })
|
|
3886
4148
|
}
|
|
3887
4149
|
),
|
|
3888
4150
|
separator,
|
|
@@ -4001,6 +4263,8 @@ function WheelRadialPopCoreCore({
|
|
|
4001
4263
|
hideInfoPanelClose = false,
|
|
4002
4264
|
hideInfoPanelTitle = false,
|
|
4003
4265
|
itemCountLabel,
|
|
4266
|
+
itemCountScaleMax,
|
|
4267
|
+
nodeCountForSizing,
|
|
4004
4268
|
readOnly,
|
|
4005
4269
|
renderDangerZone,
|
|
4006
4270
|
expanded,
|
|
@@ -4010,8 +4274,10 @@ function WheelRadialPopCoreCore({
|
|
|
4010
4274
|
overlayPanel,
|
|
4011
4275
|
wheelZoom,
|
|
4012
4276
|
defaultViewport,
|
|
4277
|
+
viewportRestore,
|
|
4013
4278
|
onViewportChange,
|
|
4014
4279
|
controlsOrientation = "vertical",
|
|
4280
|
+
showZoomControls = "auto",
|
|
4015
4281
|
floatingControlsLeading,
|
|
4016
4282
|
busy,
|
|
4017
4283
|
allowWheelRotation = true,
|
|
@@ -4019,6 +4285,7 @@ function WheelRadialPopCoreCore({
|
|
|
4019
4285
|
focusNodeId: requestedFocusNodeId = null,
|
|
4020
4286
|
onFocusChange
|
|
4021
4287
|
}) {
|
|
4288
|
+
const fullNodeCountForSizing = nodeCountForSizing ?? nodes.length;
|
|
4022
4289
|
const index = useTreeIndex(nodes);
|
|
4023
4290
|
const focusNodeId = (0, import_react13.useMemo)(
|
|
4024
4291
|
() => requestedFocusNodeId !== null && isZoomableNode(nodes, requestedFocusNodeId) ? requestedFocusNodeId : null,
|
|
@@ -4087,7 +4354,8 @@ function WheelRadialPopCoreCore({
|
|
|
4087
4354
|
const viewportRef = (0, import_react13.useRef)(null);
|
|
4088
4355
|
const stageRef = (0, import_react13.useRef)(null);
|
|
4089
4356
|
const dotRadiusMemoryRef = (0, import_react13.useRef)(/* @__PURE__ */ new Map());
|
|
4090
|
-
const
|
|
4357
|
+
const viewportFrame = (0, import_react13.useMemo)(() => JSON.stringify([focusNodeId, nodes.map(({ id, parentId, side }) => [id, parentId, side])]), [nodes, focusNodeId]);
|
|
4358
|
+
const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, viewportRestore, selectedNodeId, onViewportChange, frameKey: viewportFrame });
|
|
4091
4359
|
const { panel: openPanel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId, onSelectedNodeChange });
|
|
4092
4360
|
const isInFrame = (id) => id === focusNodeId || centerSubtreeNodeIds.has(id) || ringRootById.has(id);
|
|
4093
4361
|
const panel = openPanel && isInFrame(openPanel.node.id) ? openPanel : null;
|
|
@@ -4158,8 +4426,8 @@ function WheelRadialPopCoreCore({
|
|
|
4158
4426
|
() => getItemCountRange([
|
|
4159
4427
|
...nodes,
|
|
4160
4428
|
...[...rootItemCountById.values()].map((itemCount) => ({ itemCount }))
|
|
4161
|
-
]),
|
|
4162
|
-
[nodes, rootItemCountById]
|
|
4429
|
+
], { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }),
|
|
4430
|
+
[nodes, rootItemCountById, itemCountScaleMax, fullNodeCountForSizing]
|
|
4163
4431
|
);
|
|
4164
4432
|
const isCenterMainstream = isMainstreamPopLineage(index, centerNode.id);
|
|
4165
4433
|
const centerNodeColor = isCenterMainstream ? "#ffffff" : getGenreNodeStyle(index, centerNode.id).fill;
|
|
@@ -4193,8 +4461,8 @@ function WheelRadialPopCoreCore({
|
|
|
4193
4461
|
centerSubtreeHierarchy,
|
|
4194
4462
|
0,
|
|
4195
4463
|
POP_TREE_DEPTH_RADIAL_SPACING
|
|
4196
|
-
) : NODE_LAYOUT_SLOT_RADIUS,
|
|
4197
|
-
[centerSubtreeHierarchy]
|
|
4464
|
+
) : wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS,
|
|
4465
|
+
[centerSubtreeHierarchy, wheelItemCountRange.maxDotRadius]
|
|
4198
4466
|
);
|
|
4199
4467
|
const maxCoreExtentDelta = (0, import_react13.useMemo)(() => {
|
|
4200
4468
|
let extent = 0;
|
|
@@ -4216,7 +4484,8 @@ function WheelRadialPopCoreCore({
|
|
|
4216
4484
|
(radius) => new Map(
|
|
4217
4485
|
groups.map((group) => {
|
|
4218
4486
|
const { pop, core } = branchHierarchiesByRootId.get(group.root.id);
|
|
4219
|
-
const
|
|
4487
|
+
const dotRadius = wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS;
|
|
4488
|
+
const chipWidth = 2 * dotRadius;
|
|
4220
4489
|
return [
|
|
4221
4490
|
group.root.id,
|
|
4222
4491
|
{
|
|
@@ -4224,14 +4493,16 @@ function WheelRadialPopCoreCore({
|
|
|
4224
4493
|
(hierarchy) => measureWedge(
|
|
4225
4494
|
d34,
|
|
4226
4495
|
hierarchy,
|
|
4227
|
-
popRadiusOfDepth(radius, POP_TREE_DEPTH_RADIAL_SPACING)
|
|
4496
|
+
popRadiusOfDepth(radius, POP_TREE_DEPTH_RADIAL_SPACING),
|
|
4497
|
+
dotRadius
|
|
4228
4498
|
)
|
|
4229
4499
|
),
|
|
4230
4500
|
core: core.map(
|
|
4231
4501
|
(hierarchy) => measureWedge(
|
|
4232
4502
|
d34,
|
|
4233
4503
|
hierarchy,
|
|
4234
|
-
coreRadiusOfDepth(radius, POP_TREE_DEPTH_RADIAL_SPACING)
|
|
4504
|
+
coreRadiusOfDepth(radius, POP_TREE_DEPTH_RADIAL_SPACING),
|
|
4505
|
+
dotRadius
|
|
4235
4506
|
)
|
|
4236
4507
|
),
|
|
4237
4508
|
chip: 2 * Math.asin(Math.min(1, (chipWidth / 2 + CHIP_GAP) / radius))
|
|
@@ -4239,7 +4510,7 @@ function WheelRadialPopCoreCore({
|
|
|
4239
4510
|
];
|
|
4240
4511
|
})
|
|
4241
4512
|
),
|
|
4242
|
-
[groups, branchHierarchiesByRootId]
|
|
4513
|
+
[groups, branchHierarchiesByRootId, wheelItemCountRange.maxDotRadius]
|
|
4243
4514
|
);
|
|
4244
4515
|
const coreRootCircleRadius = (0, import_react13.useMemo)(
|
|
4245
4516
|
() => solveRingRadius(
|
|
@@ -4249,11 +4520,11 @@ function WheelRadialPopCoreCore({
|
|
|
4249
4520
|
),
|
|
4250
4521
|
Math.max(
|
|
4251
4522
|
WHEEL_POP_CORE_RADIUS,
|
|
4252
|
-
mainstreamCircleRadius + NODE_DOT_MAX_RADIUS + POP_SUBTREE_OUTER_MARGIN,
|
|
4523
|
+
mainstreamCircleRadius + (wheelItemCountRange.maxDotRadius ?? NODE_DOT_MAX_RADIUS) + POP_SUBTREE_OUTER_MARGIN,
|
|
4253
4524
|
popReachRequiredRadius
|
|
4254
4525
|
)
|
|
4255
4526
|
),
|
|
4256
|
-
[measureBranchNeedsAt, mainstreamCircleRadius, popReachRequiredRadius]
|
|
4527
|
+
[measureBranchNeedsAt, mainstreamCircleRadius, popReachRequiredRadius, wheelItemCountRange.maxDotRadius]
|
|
4257
4528
|
);
|
|
4258
4529
|
const branchNeedsByRootId = (0, import_react13.useMemo)(
|
|
4259
4530
|
() => measureBranchNeedsAt(coreRootCircleRadius),
|
|
@@ -4357,7 +4628,8 @@ function WheelRadialPopCoreCore({
|
|
|
4357
4628
|
angle,
|
|
4358
4629
|
coreRootCircleRadius,
|
|
4359
4630
|
span,
|
|
4360
|
-
POP_TREE_DEPTH_RADIAL_SPACING
|
|
4631
|
+
POP_TREE_DEPTH_RADIAL_SPACING,
|
|
4632
|
+
wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS
|
|
4361
4633
|
);
|
|
4362
4634
|
const reparentForbiddenIds = reparentingNodeId ? laidOut.descendants().find((d) => d.data.id === reparentingNodeId)?.descendants().map((d) => d.data.id) ?? [] : [];
|
|
4363
4635
|
const branchGroup = sectorGroup.append("g").attr("class", "gtv-wheel-pop-branch").attr("data-gtv-branch-index", branchIndex);
|
|
@@ -4423,7 +4695,8 @@ function WheelRadialPopCoreCore({
|
|
|
4423
4695
|
angle,
|
|
4424
4696
|
span,
|
|
4425
4697
|
coreRootCircleRadius,
|
|
4426
|
-
POP_TREE_DEPTH_RADIAL_SPACING
|
|
4698
|
+
POP_TREE_DEPTH_RADIAL_SPACING,
|
|
4699
|
+
wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS
|
|
4427
4700
|
);
|
|
4428
4701
|
const reparentForbiddenIds = reparentingNodeId ? laidOut.descendants().find((d) => d.data.id === reparentingNodeId)?.descendants().map((d) => d.data.id) ?? [] : [];
|
|
4429
4702
|
const angleOf = (d) => {
|
|
@@ -4587,7 +4860,7 @@ function WheelRadialPopCoreCore({
|
|
|
4587
4860
|
originGroup.selectAll("g.gtv-root-label").select("text").style("pointer-events", "all").style("cursor", "pointer").on("click", (_event, d) => {
|
|
4588
4861
|
const chip = d.id === centerNode.id ? centerChipRef.current : viewportRef.current?.querySelector(`#group-${CSS.escape(d.id)}`);
|
|
4589
4862
|
chip?.click();
|
|
4590
|
-
}).on("dblclick", (_event, d) => focusOnNodeRef.current(d.id));
|
|
4863
|
+
}).on("dblclick", (_event, d) => chipClickRef.current.doubleClick(() => focusOnNodeRef.current(d.id)));
|
|
4591
4864
|
}, [
|
|
4592
4865
|
centerNode,
|
|
4593
4866
|
centerHasPopDisc,
|
|
@@ -4686,18 +4959,12 @@ function WheelRadialPopCoreCore({
|
|
|
4686
4959
|
index,
|
|
4687
4960
|
regionLabel
|
|
4688
4961
|
]);
|
|
4689
|
-
const hasInitialFitRef = (0, import_react13.useRef)(false);
|
|
4690
4962
|
(0, import_react13.useEffect)(() => {
|
|
4691
|
-
hasInitialFitRef.current = false;
|
|
4692
|
-
}, [focusNodeId]);
|
|
4693
|
-
(0, import_react13.useEffect)(() => {
|
|
4694
|
-
if (hasInitialFitRef.current) return;
|
|
4695
4963
|
const elements = [
|
|
4696
4964
|
wheelCircleRef.current,
|
|
4697
4965
|
...queryTreeContentElements(popSvgRef.current)
|
|
4698
4966
|
];
|
|
4699
4967
|
if (!elements.some(Boolean)) return;
|
|
4700
|
-
hasInitialFitRef.current = true;
|
|
4701
4968
|
panZoom.fitToFrame(elements, { silent: true });
|
|
4702
4969
|
});
|
|
4703
4970
|
const handleChipClick = (rootId) => {
|
|
@@ -4719,7 +4986,8 @@ function WheelRadialPopCoreCore({
|
|
|
4719
4986
|
panZoom.centerOnElement(
|
|
4720
4987
|
element,
|
|
4721
4988
|
ZOOM_FOCUS_SCALE,
|
|
4722
|
-
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH)
|
|
4989
|
+
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),
|
|
4990
|
+
true
|
|
4723
4991
|
);
|
|
4724
4992
|
showNodeInfo(targetNode, element, viewportRef.current, { notify: false });
|
|
4725
4993
|
}, [selectedNodeId, index, openPanel, showNodeInfo, centerSubtreeNodeIds, ringRootById]);
|
|
@@ -4746,7 +5014,7 @@ function WheelRadialPopCoreCore({
|
|
|
4746
5014
|
)
|
|
4747
5015
|
};
|
|
4748
5016
|
});
|
|
4749
|
-
}, [groups, continuousAngleByRootId, sectorSpanByRootId]);
|
|
5017
|
+
}, [groups, continuousAngleByRootId, sectorSpanByRootId, index]);
|
|
4750
5018
|
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
4751
5019
|
"div",
|
|
4752
5020
|
{
|
|
@@ -4856,9 +5124,9 @@ function WheelRadialPopCoreCore({
|
|
|
4856
5124
|
showNodeInfo(centerNode, chip, viewportRef.current);
|
|
4857
5125
|
onNodeClick?.(centerNode, nativeEvent);
|
|
4858
5126
|
}
|
|
4859
|
-
});
|
|
5127
|
+
}, () => focusOnNode(centerNode.id), centerNode.id);
|
|
4860
5128
|
},
|
|
4861
|
-
onDoubleClick: () => focusOnNode(centerNode.id),
|
|
5129
|
+
onDoubleClick: () => chipClickRef.current.doubleClick(() => focusOnNode(centerNode.id)),
|
|
4862
5130
|
"aria-label": centerNode.name
|
|
4863
5131
|
}
|
|
4864
5132
|
) }) }),
|
|
@@ -4918,9 +5186,9 @@ function WheelRadialPopCoreCore({
|
|
|
4918
5186
|
showNodeInfo(root, chip, viewportRef.current);
|
|
4919
5187
|
onNodeClick?.(root, nativeEvent);
|
|
4920
5188
|
}
|
|
4921
|
-
});
|
|
5189
|
+
}, () => focusOnNode(group.root.id), group.root.id);
|
|
4922
5190
|
},
|
|
4923
|
-
onDoubleClick: () => focusOnNode(group.root.id)
|
|
5191
|
+
onDoubleClick: () => chipClickRef.current.doubleClick(() => focusOnNode(group.root.id))
|
|
4924
5192
|
}
|
|
4925
5193
|
) })
|
|
4926
5194
|
},
|
|
@@ -5015,6 +5283,7 @@ function WheelRadialPopCoreCore({
|
|
|
5015
5283
|
{
|
|
5016
5284
|
panZoom,
|
|
5017
5285
|
orientation: controlsOrientation,
|
|
5286
|
+
zoomControls: showZoomControls,
|
|
5018
5287
|
leading: floatingControlsLeading,
|
|
5019
5288
|
busy,
|
|
5020
5289
|
onFit: () => panZoom.fitToFrame([
|
|
@@ -5216,7 +5485,7 @@ function GenreTreeOutline(props) {
|
|
|
5216
5485
|
return;
|
|
5217
5486
|
}
|
|
5218
5487
|
const changed = store.get().selectedId !== node.id;
|
|
5219
|
-
store.set({ selectedId: node.id });
|
|
5488
|
+
if (latestProps.current.selectedNodeId === void 0) store.set({ selectedId: node.id });
|
|
5220
5489
|
if (changed) latestProps.current.onSelectedNodeChange?.(node);
|
|
5221
5490
|
onNodeClick?.(node, event.nativeEvent);
|
|
5222
5491
|
},
|
|
@@ -5248,7 +5517,7 @@ function GenreTreeOutline(props) {
|
|
|
5248
5517
|
}
|
|
5249
5518
|
const changed = store.get().selectedId !== nodeId;
|
|
5250
5519
|
if (changed) pendingScrollIdRef.current = nodeId;
|
|
5251
|
-
store.set({ openIds, selectedId: nodeId });
|
|
5520
|
+
store.set({ openIds, ...notify && latestProps.current.selectedNodeId !== void 0 ? {} : { selectedId: nodeId } });
|
|
5252
5521
|
if (changed && notify) latestProps.current.onSelectedNodeChange?.(index.byId.get(nodeId));
|
|
5253
5522
|
};
|
|
5254
5523
|
const selectNodeRef = (0, import_react14.useRef)(selectNode);
|
|
@@ -5289,7 +5558,7 @@ function GenreTreeOutline(props) {
|
|
|
5289
5558
|
ancestorNodes: index.ancestorsOf(selectedNode.id).map((n) => ({ node: n, ...model.styleOf(n) })),
|
|
5290
5559
|
side: "left",
|
|
5291
5560
|
onClose: () => {
|
|
5292
|
-
store.set({ selectedId: null });
|
|
5561
|
+
if (latestProps.current.selectedNodeId === void 0) store.set({ selectedId: null });
|
|
5293
5562
|
latestProps.current.onSelectedNodeChange?.(null);
|
|
5294
5563
|
},
|
|
5295
5564
|
onSelectNode: (id) => selectNodeRef.current(id),
|