@behindthemusictree/genre-tree-view 6.5.1 → 7.1.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 +201 -240
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -8
- package/dist/index.d.ts +84 -8
- package/dist/index.js +192 -236
- package/dist/index.js.map +1 -1
- package/dist/styles.css +21 -14
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
+
ACCENT_TEXT_COLOR: () => ACCENT_TEXT_COLOR,
|
|
33
34
|
DEFAULT_FRAME_HEIGHT: () => DEFAULT_FRAME_HEIGHT,
|
|
34
35
|
DEFAULT_FRAME_WIDTH: () => DEFAULT_FRAME_WIDTH,
|
|
35
36
|
GenreTree: () => GenreTree,
|
|
@@ -42,9 +43,13 @@ __export(src_exports, {
|
|
|
42
43
|
GenreTreeWheelRadialPopCore: () => GenreTreeWheelRadialPopCore,
|
|
43
44
|
GenreTreeWheelRight: () => GenreTreeWheelRight,
|
|
44
45
|
GenreTreeWheelSkeleton: () => GenreTreeWheelSkeleton,
|
|
46
|
+
InfoPanel: () => InfoPanel,
|
|
47
|
+
ROOT_TINT_RATIO: () => ROOT_TINT_RATIO,
|
|
48
|
+
TEXT_COLOR: () => TEXT_COLOR,
|
|
45
49
|
getGenreTreeColor: () => getGenreTreeColor,
|
|
46
50
|
groupNodesByRoot: () => groupNodesByRoot,
|
|
47
|
-
isZoomableNode: () => isZoomableNode
|
|
51
|
+
isZoomableNode: () => isZoomableNode,
|
|
52
|
+
tintSurface: () => tintSurface
|
|
48
53
|
});
|
|
49
54
|
module.exports = __toCommonJS(src_exports);
|
|
50
55
|
|
|
@@ -57,6 +62,7 @@ var import_server = __toESM(require("react-dom/server"), 1);
|
|
|
57
62
|
var import_pi = require("react-icons/pi");
|
|
58
63
|
|
|
59
64
|
// src/constants.ts
|
|
65
|
+
var import_ui = require("@behindthemusictree/ui");
|
|
60
66
|
var import_d3 = require("d3");
|
|
61
67
|
var SURFACE_BORDER_COLOR = "#E4E4E7";
|
|
62
68
|
var CORNER_RADIUS = 8;
|
|
@@ -76,7 +82,6 @@ var ROOT_TINT_RATIO = 0.08;
|
|
|
76
82
|
var POP_SECTOR_TINT_RATIO = 0.8;
|
|
77
83
|
var UNDERGROUND_DARKEN_RATIO = 0.62;
|
|
78
84
|
var UNDERGROUND_SECTOR_FILL_OPACITY = 0.15;
|
|
79
|
-
var SCENE_SECTOR_OUTER_RADIUS = 1e5;
|
|
80
85
|
var CENTER_NODE_NAME = "Mainstream Pop";
|
|
81
86
|
var TREE_COLORS = [
|
|
82
87
|
"#4F46E5",
|
|
@@ -228,11 +233,7 @@ function getItemCountRange(nodes, options = {}) {
|
|
|
228
233
|
};
|
|
229
234
|
}
|
|
230
235
|
function logarithmicPosition(itemCount, range) {
|
|
231
|
-
|
|
232
|
-
const logMin = Math.log1p(Math.max(min, 0));
|
|
233
|
-
const logMax = Math.log1p(Math.max(max, 0));
|
|
234
|
-
const logValue = Math.log1p(Math.max(itemCount, 0));
|
|
235
|
-
return logMax > logMin ? Math.max(0, Math.min(1, (logValue - logMin) / (logMax - logMin))) : 0;
|
|
236
|
+
return (0, import_ui.logScalePosition)(itemCount, range);
|
|
236
237
|
}
|
|
237
238
|
function calculateNodeDimensions(itemCount, range) {
|
|
238
239
|
const t = logarithmicPosition(itemCount, range);
|
|
@@ -271,8 +272,11 @@ function calculateLabelRank(itemCount, range, depth) {
|
|
|
271
272
|
return logarithmicPosition(itemCount, range) - Math.max(0, depth - 1) * LABEL_DEPTH_RANK_PENALTY;
|
|
272
273
|
}
|
|
273
274
|
function isLabelRankedForZoom(rank, zoomScale) {
|
|
274
|
-
|
|
275
|
-
|
|
275
|
+
return (0, import_ui.isRankShownAtZoom)(rank, zoomScale, {
|
|
276
|
+
minScale: ZOOM_MIN_SCALE,
|
|
277
|
+
focusScale: ZOOM_FOCUS_SCALE,
|
|
278
|
+
minRankAtMinZoom: LABEL_MIN_RANK_AT_MIN_ZOOM
|
|
279
|
+
});
|
|
276
280
|
}
|
|
277
281
|
function calculateRootLabelFontSize(layoutFontSize, zoomScale) {
|
|
278
282
|
return Math.max(layoutFontSize, ROOT_LABEL_MIN_SCREEN_PX / zoomScale);
|
|
@@ -704,7 +708,7 @@ function renderTree(d3Lib, svgRef, treeData, svgWidth, svgHeight, reparentingNod
|
|
|
704
708
|
}
|
|
705
709
|
|
|
706
710
|
// src/FloatingControls.tsx
|
|
707
|
-
var
|
|
711
|
+
var import_ui2 = require("@behindthemusictree/ui");
|
|
708
712
|
|
|
709
713
|
// src/BusyIndicator.tsx
|
|
710
714
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
@@ -717,32 +721,22 @@ function BusyIndicator() {
|
|
|
717
721
|
|
|
718
722
|
// src/FloatingControls.tsx
|
|
719
723
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
720
|
-
function FloatingControls({ panZoom, onFit,
|
|
721
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
722
|
-
|
|
723
|
-
{
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
)
|
|
743
|
-
]
|
|
744
|
-
}
|
|
745
|
-
);
|
|
724
|
+
function FloatingControls({ panZoom, onFit, zoomControls, leading, busy }) {
|
|
725
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "gtv-wheel-floating-controls", children: [
|
|
726
|
+
busy && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BusyIndicator, {}),
|
|
727
|
+
leading && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "gtv-floating-controls-leading", children: leading }),
|
|
728
|
+
zoomControls !== "never" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
729
|
+
import_ui2.ZoomControls,
|
|
730
|
+
{
|
|
731
|
+
onZoomIn: panZoom.zoomIn,
|
|
732
|
+
onZoomOut: panZoom.zoomOut,
|
|
733
|
+
onFit,
|
|
734
|
+
canZoomIn: panZoom.canZoomIn,
|
|
735
|
+
canZoomOut: panZoom.canZoomOut,
|
|
736
|
+
className: zoomControls === "auto" ? "gtv-zoom-controls gtv-zoom-controls--auto" : "gtv-zoom-controls"
|
|
737
|
+
}
|
|
738
|
+
)
|
|
739
|
+
] });
|
|
746
740
|
}
|
|
747
741
|
|
|
748
742
|
// src/use-pan-zoom.ts
|
|
@@ -900,8 +894,8 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
900
894
|
if (!viewport || !bounds) return pan;
|
|
901
895
|
const origin = axis === "x" ? bounds.originX : bounds.originY;
|
|
902
896
|
const size = axis === "x" ? bounds.width : bounds.height;
|
|
903
|
-
const
|
|
904
|
-
const panMax =
|
|
897
|
+
const viewportSize2 = axis === "x" ? viewport.clientWidth : viewport.clientHeight;
|
|
898
|
+
const panMax = viewportSize2 - PAN_MIN_VISIBLE_PX - origin * scale;
|
|
905
899
|
const panMin = PAN_MIN_VISIBLE_PX - size * scale - origin * scale;
|
|
906
900
|
const lo = Math.min(panMin, panMax);
|
|
907
901
|
const hi = Math.max(panMin, panMax);
|
|
@@ -989,6 +983,7 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
989
983
|
[stepZoomAnimation]
|
|
990
984
|
);
|
|
991
985
|
const viewportSizeRef = (0, import_react.useRef)(null);
|
|
986
|
+
const [viewportSize, setViewportSize] = (0, import_react.useState)(null);
|
|
992
987
|
(0, import_react.useEffect)(() => {
|
|
993
988
|
const viewport = viewportRef.current;
|
|
994
989
|
if (!viewport) return;
|
|
@@ -1001,6 +996,9 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1001
996
|
const { width, height } = entry.contentRect;
|
|
1002
997
|
const previous = viewportSizeRef.current;
|
|
1003
998
|
viewportSizeRef.current = { width, height };
|
|
999
|
+
if (width > 0 && height > 0) {
|
|
1000
|
+
setViewportSize((prev) => prev?.width === width && prev.height === height ? prev : { width, height });
|
|
1001
|
+
}
|
|
1004
1002
|
if (!previous || previous.width <= 0 || previous.height <= 0) {
|
|
1005
1003
|
retryFrameRef.current();
|
|
1006
1004
|
return;
|
|
@@ -1205,8 +1203,8 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1205
1203
|
(element, targetScale, obscured, automatic = false) => {
|
|
1206
1204
|
if (automatic && restoredRevisionRef.current !== void 0 && restoredFrameRef.current === options.frameKey && restoredSelectionRef.current === options.selectedNodeId) return;
|
|
1207
1205
|
if (!automatic) operationRef.current += 1;
|
|
1208
|
-
const
|
|
1209
|
-
if (automatic && (measuredFrameRef.current !== options.frameKey || fittedFrameRef.current === null || !
|
|
1206
|
+
const viewportSize2 = viewportRef.current?.getBoundingClientRect();
|
|
1207
|
+
if (automatic && (measuredFrameRef.current !== options.frameKey || fittedFrameRef.current === null || !viewportSize2?.width || !viewportSize2.height)) {
|
|
1210
1208
|
const operation = operationRef.current;
|
|
1211
1209
|
pendingCenterRef.current = () => {
|
|
1212
1210
|
if (operation !== operationRef.current) return;
|
|
@@ -1407,6 +1405,8 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1407
1405
|
panX,
|
|
1408
1406
|
panY,
|
|
1409
1407
|
zoomScale,
|
|
1408
|
+
minScale,
|
|
1409
|
+
viewportSize,
|
|
1410
1410
|
transform: `translate(${panX}px, ${panY}px) scale(${zoomScale})`,
|
|
1411
1411
|
canZoomIn: zoomScale < ZOOM_MAX_SCALE,
|
|
1412
1412
|
canZoomOut: zoomScale > minScale,
|
|
@@ -2052,7 +2052,6 @@ function GenreTree({
|
|
|
2052
2052
|
defaultViewport,
|
|
2053
2053
|
viewportRestore,
|
|
2054
2054
|
onViewportChange,
|
|
2055
|
-
controlsOrientation = "vertical",
|
|
2056
2055
|
showZoomControls = "auto",
|
|
2057
2056
|
floatingControlsLeading,
|
|
2058
2057
|
busy
|
|
@@ -2347,7 +2346,6 @@ function GenreTree({
|
|
|
2347
2346
|
FloatingControls,
|
|
2348
2347
|
{
|
|
2349
2348
|
panZoom,
|
|
2350
|
-
orientation: controlsOrientation,
|
|
2351
2349
|
zoomControls: showZoomControls,
|
|
2352
2350
|
leading: floatingControlsLeading,
|
|
2353
2351
|
busy,
|
|
@@ -2435,6 +2433,121 @@ function computeRotationForSelection(currentRotationDeg, targetAngle, landingAng
|
|
|
2435
2433
|
return currentRotationDeg + shortestDelta;
|
|
2436
2434
|
}
|
|
2437
2435
|
|
|
2436
|
+
// src/radial-wheel-geometry.ts
|
|
2437
|
+
function computeRadialLayout(weights, clickedIndex, landingAngle) {
|
|
2438
|
+
const rootCount = weights.length;
|
|
2439
|
+
if (rootCount <= 0) return [];
|
|
2440
|
+
if (weights.some((weight) => weight <= 0)) {
|
|
2441
|
+
throw new Error("computeRadialLayout: every root weight (node count) must be positive");
|
|
2442
|
+
}
|
|
2443
|
+
const total = weights.reduce((sum2, weight) => sum2 + weight, 0);
|
|
2444
|
+
const orderedWidths = weights.map((_, i) => weights[(clickedIndex + i) % rootCount] / total * 360);
|
|
2445
|
+
const slots = new Array(rootCount);
|
|
2446
|
+
let boundary = landingAngle - orderedWidths[0] / 2;
|
|
2447
|
+
for (let i = 0; i < rootCount; i++) {
|
|
2448
|
+
const width = orderedWidths[i];
|
|
2449
|
+
const rootIndex = (clickedIndex + i) % rootCount;
|
|
2450
|
+
slots[rootIndex] = { rootIndex, angle: ((boundary + width / 2) % 360 + 360) % 360 };
|
|
2451
|
+
boundary += width;
|
|
2452
|
+
}
|
|
2453
|
+
return slots;
|
|
2454
|
+
}
|
|
2455
|
+
function computeSectorWidths(weights) {
|
|
2456
|
+
const total = weights.reduce((sum2, weight) => sum2 + weight, 0);
|
|
2457
|
+
return weights.map((weight) => weight / total * 360);
|
|
2458
|
+
}
|
|
2459
|
+
function subdivideWedge(wedgeCenterDeg, wedgeSpanDeg, weights) {
|
|
2460
|
+
if (weights.length <= 0) return [];
|
|
2461
|
+
const total = weights.reduce((sum2, weight) => sum2 + weight, 0);
|
|
2462
|
+
const spans = weights.map((weight) => weight / total * wedgeSpanDeg);
|
|
2463
|
+
const slices = [];
|
|
2464
|
+
let boundary = wedgeCenterDeg - wedgeSpanDeg / 2;
|
|
2465
|
+
for (const spanDeg of spans) {
|
|
2466
|
+
slices.push({ centerDeg: boundary + spanDeg / 2, spanDeg });
|
|
2467
|
+
boundary += spanDeg;
|
|
2468
|
+
}
|
|
2469
|
+
return slices;
|
|
2470
|
+
}
|
|
2471
|
+
function coverageRadius(viewportWidth, viewportHeight, minScale, contentRadius) {
|
|
2472
|
+
if (!(minScale > 0)) throw new Error("coverageRadius: minScale must be positive");
|
|
2473
|
+
return Math.ceil(contentRadius * Math.SQRT2 + Math.hypot(viewportWidth, viewportHeight) / minScale);
|
|
2474
|
+
}
|
|
2475
|
+
function buildSectorClipPathPolygon(sweepDeg, stepDeg = 10) {
|
|
2476
|
+
if (sweepDeg <= 0) return "";
|
|
2477
|
+
const points = ["50% 50%"];
|
|
2478
|
+
const steps = Math.max(1, Math.ceil(sweepDeg / stepDeg));
|
|
2479
|
+
for (let i = 0; i <= steps; i++) {
|
|
2480
|
+
const theta = Math.min(i * stepDeg, sweepDeg) * Math.PI / 180;
|
|
2481
|
+
const x = 50 + 50 * Math.sin(theta);
|
|
2482
|
+
const y = 50 - 50 * Math.cos(theta);
|
|
2483
|
+
points.push(`${x}% ${y}%`);
|
|
2484
|
+
}
|
|
2485
|
+
return `polygon(${points.join(", ")})`;
|
|
2486
|
+
}
|
|
2487
|
+
function calculateWheelRadiusForAngles(angles, maxChipWidth, baseRadius) {
|
|
2488
|
+
if (angles.length < 2) return baseRadius;
|
|
2489
|
+
const sorted = [...angles].sort((a, b) => a - b);
|
|
2490
|
+
let minGapDeg = 360;
|
|
2491
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
2492
|
+
const next = i < sorted.length - 1 ? sorted[i + 1] : sorted[0] + 360;
|
|
2493
|
+
minGapDeg = Math.min(minGapDeg, next - sorted[i]);
|
|
2494
|
+
}
|
|
2495
|
+
if (minGapDeg <= 0) return baseRadius;
|
|
2496
|
+
const CHIP_GAP2 = 16;
|
|
2497
|
+
const requiredRadius = (maxChipWidth + CHIP_GAP2) / (2 * Math.sin(minGapDeg * Math.PI / 360));
|
|
2498
|
+
return Math.max(baseRadius, requiredRadius);
|
|
2499
|
+
}
|
|
2500
|
+
function solveRingRadius(angleNeededAt, floorRadius) {
|
|
2501
|
+
const fits = (radius) => angleNeededAt(radius) <= 2 * Math.PI;
|
|
2502
|
+
if (fits(floorRadius)) return floorRadius;
|
|
2503
|
+
let lo = floorRadius;
|
|
2504
|
+
let hi = floorRadius * 2;
|
|
2505
|
+
while (!fits(hi)) {
|
|
2506
|
+
lo = hi;
|
|
2507
|
+
hi *= 2;
|
|
2508
|
+
}
|
|
2509
|
+
while (hi - lo > 1) {
|
|
2510
|
+
const mid = (lo + hi) / 2;
|
|
2511
|
+
if (fits(mid)) hi = mid;
|
|
2512
|
+
else lo = mid;
|
|
2513
|
+
}
|
|
2514
|
+
return hi;
|
|
2515
|
+
}
|
|
2516
|
+
var boxesOverlap = (a, b) => a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;
|
|
2517
|
+
function findSectorAtAngle(angle, sectors) {
|
|
2518
|
+
const sector = sectors.find(
|
|
2519
|
+
({ center, span }) => Math.abs(((angle - center) % 360 + 540) % 360 - 180) <= span / 2
|
|
2520
|
+
);
|
|
2521
|
+
return sector?.id ?? null;
|
|
2522
|
+
}
|
|
2523
|
+
var boxGap = (a, b) => Math.hypot(Math.max(0, a.x0 - b.x1, b.x0 - a.x1), Math.max(0, a.y0 - b.y1, b.y0 - a.y1));
|
|
2524
|
+
function pickEmptyLabelSpot(area, width, height, obstacles, prefer = () => true) {
|
|
2525
|
+
const STEPS = 9;
|
|
2526
|
+
const spanX = area.x1 - area.x0 - width;
|
|
2527
|
+
const spanY = area.y1 - area.y0 - height;
|
|
2528
|
+
if (spanX < 0 || spanY < 0) return null;
|
|
2529
|
+
const centerX = (area.x0 + area.x1) / 2;
|
|
2530
|
+
const centerY = (area.y0 + area.y1) / 2;
|
|
2531
|
+
let best = null;
|
|
2532
|
+
for (let i = 0; i < STEPS; i++) {
|
|
2533
|
+
for (let j = 0; j < STEPS; j++) {
|
|
2534
|
+
const x = area.x0 + width / 2 + spanX * i / (STEPS - 1);
|
|
2535
|
+
const y = area.y0 + height / 2 + spanY * j / (STEPS - 1);
|
|
2536
|
+
const box = { x0: x - width / 2, x1: x + width / 2, y0: y - height / 2, y1: y + height / 2 };
|
|
2537
|
+
if (obstacles.some((obstacle) => boxesOverlap(box, obstacle))) continue;
|
|
2538
|
+
const gap = Math.min(height, ...obstacles.map((obstacle) => boxGap(box, obstacle)));
|
|
2539
|
+
const score = [
|
|
2540
|
+
prefer(x, y) ? 1 : 0,
|
|
2541
|
+
gap,
|
|
2542
|
+
-Math.hypot(x - centerX, y - centerY)
|
|
2543
|
+
];
|
|
2544
|
+
const k = best ? score.findIndex((value, index) => value !== best.score[index]) : 0;
|
|
2545
|
+
if (!best || k >= 0 && score[k] > best.score[k]) best = { x, y, score };
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
return best && { x: best.x, y: best.y };
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2438
2551
|
// src/GenreTreeWheelBase.tsx
|
|
2439
2552
|
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
2440
2553
|
function WheelCore({
|
|
@@ -2474,7 +2587,6 @@ function WheelCore({
|
|
|
2474
2587
|
defaultViewport,
|
|
2475
2588
|
viewportRestore,
|
|
2476
2589
|
onViewportChange,
|
|
2477
|
-
controlsOrientation = "vertical",
|
|
2478
2590
|
showZoomControls = "auto",
|
|
2479
2591
|
floatingControlsLeading,
|
|
2480
2592
|
busy,
|
|
@@ -2516,6 +2628,8 @@ function WheelCore({
|
|
|
2516
2628
|
const stageRef = (0, import_react9.useRef)(null);
|
|
2517
2629
|
const viewportFrame = (0, import_react9.useMemo)(() => JSON.stringify(nodes.map(({ id, parentId, side }) => [id, parentId, side])), [nodes]);
|
|
2518
2630
|
const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, viewportRestore, selectedNodeId, onViewportChange, frameKey: viewportFrame });
|
|
2631
|
+
const { viewportSize, minScale } = panZoom;
|
|
2632
|
+
const wheelCoverageRadius = viewportSize ? coverageRadius(viewportSize.width, viewportSize.height, minScale, wheelRadius) : null;
|
|
2519
2633
|
const { panel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId, onSelectedNodeChange });
|
|
2520
2634
|
const wheelCircleRef = (0, import_react9.useRef)(null);
|
|
2521
2635
|
const treeAnchorRef = (0, import_react9.useRef)(null);
|
|
@@ -2587,7 +2701,7 @@ function WheelCore({
|
|
|
2587
2701
|
(0, import_react9.useEffect)(() => {
|
|
2588
2702
|
if (!selectedNodeId || panel?.node.id === selectedNodeId) return;
|
|
2589
2703
|
const targetNode = nodes.find((node) => node.id === selectedNodeId);
|
|
2590
|
-
const element =
|
|
2704
|
+
const element = viewportRef.current?.querySelector(
|
|
2591
2705
|
`#group-${CSS.escape(selectedNodeId)}`
|
|
2592
2706
|
);
|
|
2593
2707
|
if (!targetNode || !element) return;
|
|
@@ -2611,6 +2725,7 @@ function WheelCore({
|
|
|
2611
2725
|
].filter(Boolean).join(" "),
|
|
2612
2726
|
style: {
|
|
2613
2727
|
"--gtv-wheel-radius": `${wheelRadius}px`,
|
|
2728
|
+
...wheelCoverageRadius !== null && { "--gtv-wheel-coverage-radius": `${wheelCoverageRadius}px` },
|
|
2614
2729
|
"--gtv-wheel-viewport-height": `${wheelViewportHeight}px`,
|
|
2615
2730
|
"--gtv-wheel-rotation-transition-ms": `${WHEEL_ROTATION_TRANSITION_MS}ms`,
|
|
2616
2731
|
"--gtv-wheel-rotation-easing": WHEEL_ROTATION_EASING,
|
|
@@ -2856,7 +2971,6 @@ function WheelCore({
|
|
|
2856
2971
|
FloatingControls,
|
|
2857
2972
|
{
|
|
2858
2973
|
panZoom,
|
|
2859
|
-
orientation: controlsOrientation,
|
|
2860
2974
|
zoomControls: showZoomControls,
|
|
2861
2975
|
leading: floatingControlsLeading,
|
|
2862
2976
|
busy,
|
|
@@ -2887,6 +3001,9 @@ function GenreTreeWheelRight(props) {
|
|
|
2887
3001
|
var import_react10 = require("react");
|
|
2888
3002
|
var d33 = __toESM(require("d3"), 1);
|
|
2889
3003
|
|
|
3004
|
+
// src/pop-core-radial-layout.ts
|
|
3005
|
+
var import_ui3 = require("@behindthemusictree/ui");
|
|
3006
|
+
|
|
2890
3007
|
// src/defer-click.ts
|
|
2891
3008
|
function createDeferredClick() {
|
|
2892
3009
|
let timeoutId = null;
|
|
@@ -2918,166 +3035,6 @@ function createDeferredClick() {
|
|
|
2918
3035
|
return { schedule, cancel, doubleClick };
|
|
2919
3036
|
}
|
|
2920
3037
|
|
|
2921
|
-
// src/radial-wheel-geometry.ts
|
|
2922
|
-
function computeRadialLayout(weights, clickedIndex, landingAngle) {
|
|
2923
|
-
const rootCount = weights.length;
|
|
2924
|
-
if (rootCount <= 0) return [];
|
|
2925
|
-
if (weights.some((weight) => weight <= 0)) {
|
|
2926
|
-
throw new Error("computeRadialLayout: every root weight (node count) must be positive");
|
|
2927
|
-
}
|
|
2928
|
-
const total = weights.reduce((sum2, weight) => sum2 + weight, 0);
|
|
2929
|
-
const orderedWidths = weights.map((_, i) => weights[(clickedIndex + i) % rootCount] / total * 360);
|
|
2930
|
-
const slots = new Array(rootCount);
|
|
2931
|
-
let boundary = landingAngle - orderedWidths[0] / 2;
|
|
2932
|
-
for (let i = 0; i < rootCount; i++) {
|
|
2933
|
-
const width = orderedWidths[i];
|
|
2934
|
-
const rootIndex = (clickedIndex + i) % rootCount;
|
|
2935
|
-
slots[rootIndex] = { rootIndex, angle: ((boundary + width / 2) % 360 + 360) % 360 };
|
|
2936
|
-
boundary += width;
|
|
2937
|
-
}
|
|
2938
|
-
return slots;
|
|
2939
|
-
}
|
|
2940
|
-
function computeSectorWidths(weights) {
|
|
2941
|
-
const total = weights.reduce((sum2, weight) => sum2 + weight, 0);
|
|
2942
|
-
return weights.map((weight) => weight / total * 360);
|
|
2943
|
-
}
|
|
2944
|
-
function subdivideWedge(wedgeCenterDeg, wedgeSpanDeg, weights) {
|
|
2945
|
-
if (weights.length <= 0) return [];
|
|
2946
|
-
const total = weights.reduce((sum2, weight) => sum2 + weight, 0);
|
|
2947
|
-
const spans = weights.map((weight) => weight / total * wedgeSpanDeg);
|
|
2948
|
-
const slices = [];
|
|
2949
|
-
let boundary = wedgeCenterDeg - wedgeSpanDeg / 2;
|
|
2950
|
-
for (const spanDeg of spans) {
|
|
2951
|
-
slices.push({ centerDeg: boundary + spanDeg / 2, spanDeg });
|
|
2952
|
-
boundary += spanDeg;
|
|
2953
|
-
}
|
|
2954
|
-
return slices;
|
|
2955
|
-
}
|
|
2956
|
-
function buildSectorClipPathPolygon(sweepDeg, stepDeg = 10) {
|
|
2957
|
-
if (sweepDeg <= 0) return "";
|
|
2958
|
-
const points = ["50% 50%"];
|
|
2959
|
-
const steps = Math.max(1, Math.ceil(sweepDeg / stepDeg));
|
|
2960
|
-
for (let i = 0; i <= steps; i++) {
|
|
2961
|
-
const theta = Math.min(i * stepDeg, sweepDeg) * Math.PI / 180;
|
|
2962
|
-
const x = 50 + 50 * Math.sin(theta);
|
|
2963
|
-
const y = 50 - 50 * Math.cos(theta);
|
|
2964
|
-
points.push(`${x}% ${y}%`);
|
|
2965
|
-
}
|
|
2966
|
-
return `polygon(${points.join(", ")})`;
|
|
2967
|
-
}
|
|
2968
|
-
function calculateWheelRadiusForAngles(angles, maxChipWidth, baseRadius) {
|
|
2969
|
-
if (angles.length < 2) return baseRadius;
|
|
2970
|
-
const sorted = [...angles].sort((a, b) => a - b);
|
|
2971
|
-
let minGapDeg = 360;
|
|
2972
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
2973
|
-
const next = i < sorted.length - 1 ? sorted[i + 1] : sorted[0] + 360;
|
|
2974
|
-
minGapDeg = Math.min(minGapDeg, next - sorted[i]);
|
|
2975
|
-
}
|
|
2976
|
-
if (minGapDeg <= 0) return baseRadius;
|
|
2977
|
-
const CHIP_GAP2 = 16;
|
|
2978
|
-
const requiredRadius = (maxChipWidth + CHIP_GAP2) / (2 * Math.sin(minGapDeg * Math.PI / 360));
|
|
2979
|
-
return Math.max(baseRadius, requiredRadius);
|
|
2980
|
-
}
|
|
2981
|
-
function solveRingRadius(angleNeededAt, floorRadius) {
|
|
2982
|
-
const fits = (radius) => angleNeededAt(radius) <= 2 * Math.PI;
|
|
2983
|
-
if (fits(floorRadius)) return floorRadius;
|
|
2984
|
-
let lo = floorRadius;
|
|
2985
|
-
let hi = floorRadius * 2;
|
|
2986
|
-
while (!fits(hi)) {
|
|
2987
|
-
lo = hi;
|
|
2988
|
-
hi *= 2;
|
|
2989
|
-
}
|
|
2990
|
-
while (hi - lo > 1) {
|
|
2991
|
-
const mid = (lo + hi) / 2;
|
|
2992
|
-
if (fits(mid)) hi = mid;
|
|
2993
|
-
else lo = mid;
|
|
2994
|
-
}
|
|
2995
|
-
return hi;
|
|
2996
|
-
}
|
|
2997
|
-
var boxesOverlap = (a, b) => a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;
|
|
2998
|
-
var GRID_CELL = 64;
|
|
2999
|
-
function createBoxGrid() {
|
|
3000
|
-
const cells = /* @__PURE__ */ new Map();
|
|
3001
|
-
const cellRange = (box) => ({
|
|
3002
|
-
cx0: Math.floor(box.x0 / GRID_CELL),
|
|
3003
|
-
cx1: Math.floor(box.x1 / GRID_CELL),
|
|
3004
|
-
cy0: Math.floor(box.y0 / GRID_CELL),
|
|
3005
|
-
cy1: Math.floor(box.y1 / GRID_CELL)
|
|
3006
|
-
});
|
|
3007
|
-
return {
|
|
3008
|
-
insert(box, value) {
|
|
3009
|
-
const { cx0, cx1, cy0, cy1 } = cellRange(box);
|
|
3010
|
-
for (let cx = cx0; cx <= cx1; cx++) {
|
|
3011
|
-
for (let cy = cy0; cy <= cy1; cy++) {
|
|
3012
|
-
const key = `${cx},${cy}`;
|
|
3013
|
-
const bucket = cells.get(key);
|
|
3014
|
-
if (bucket) bucket.push({ box, value });
|
|
3015
|
-
else cells.set(key, [{ box, value }]);
|
|
3016
|
-
}
|
|
3017
|
-
}
|
|
3018
|
-
},
|
|
3019
|
-
some(box, predicate) {
|
|
3020
|
-
const { cx0, cx1, cy0, cy1 } = cellRange(box);
|
|
3021
|
-
for (let cx = cx0; cx <= cx1; cx++) {
|
|
3022
|
-
for (let cy = cy0; cy <= cy1; cy++) {
|
|
3023
|
-
if (cells.get(`${cx},${cy}`)?.some((entry) => boxesOverlap(box, entry.box) && predicate(entry.value))) {
|
|
3024
|
-
return true;
|
|
3025
|
-
}
|
|
3026
|
-
}
|
|
3027
|
-
}
|
|
3028
|
-
return false;
|
|
3029
|
-
}
|
|
3030
|
-
};
|
|
3031
|
-
}
|
|
3032
|
-
function placeLabels(items) {
|
|
3033
|
-
const placement = /* @__PURE__ */ new Map();
|
|
3034
|
-
const keptGrid = createBoxGrid();
|
|
3035
|
-
const dotGrid = createBoxGrid();
|
|
3036
|
-
for (const item of items) dotGrid.insert(item.dot, item.id);
|
|
3037
|
-
for (const item of [...items].sort((a, b) => b.priority - a.priority)) {
|
|
3038
|
-
let index = item.labels.findIndex(
|
|
3039
|
-
(label) => !keptGrid.some(label, () => true) && !dotGrid.some(label, (id) => id !== item.id)
|
|
3040
|
-
);
|
|
3041
|
-
if (index < 0 && item.forced && item.labels.length > 0) index = 0;
|
|
3042
|
-
placement.set(item.id, index);
|
|
3043
|
-
if (index >= 0) keptGrid.insert(item.labels[index], true);
|
|
3044
|
-
}
|
|
3045
|
-
return placement;
|
|
3046
|
-
}
|
|
3047
|
-
function findSectorAtAngle(angle, sectors) {
|
|
3048
|
-
const sector = sectors.find(
|
|
3049
|
-
({ center, span }) => Math.abs(((angle - center) % 360 + 540) % 360 - 180) <= span / 2
|
|
3050
|
-
);
|
|
3051
|
-
return sector?.id ?? null;
|
|
3052
|
-
}
|
|
3053
|
-
var boxGap = (a, b) => Math.hypot(Math.max(0, a.x0 - b.x1, b.x0 - a.x1), Math.max(0, a.y0 - b.y1, b.y0 - a.y1));
|
|
3054
|
-
function pickEmptyLabelSpot(area, width, height, obstacles, prefer = () => true) {
|
|
3055
|
-
const STEPS = 9;
|
|
3056
|
-
const spanX = area.x1 - area.x0 - width;
|
|
3057
|
-
const spanY = area.y1 - area.y0 - height;
|
|
3058
|
-
if (spanX < 0 || spanY < 0) return null;
|
|
3059
|
-
const centerX = (area.x0 + area.x1) / 2;
|
|
3060
|
-
const centerY = (area.y0 + area.y1) / 2;
|
|
3061
|
-
let best = null;
|
|
3062
|
-
for (let i = 0; i < STEPS; i++) {
|
|
3063
|
-
for (let j = 0; j < STEPS; j++) {
|
|
3064
|
-
const x = area.x0 + width / 2 + spanX * i / (STEPS - 1);
|
|
3065
|
-
const y = area.y0 + height / 2 + spanY * j / (STEPS - 1);
|
|
3066
|
-
const box = { x0: x - width / 2, x1: x + width / 2, y0: y - height / 2, y1: y + height / 2 };
|
|
3067
|
-
if (obstacles.some((obstacle) => boxesOverlap(box, obstacle))) continue;
|
|
3068
|
-
const gap = Math.min(height, ...obstacles.map((obstacle) => boxGap(box, obstacle)));
|
|
3069
|
-
const score = [
|
|
3070
|
-
prefer(x, y) ? 1 : 0,
|
|
3071
|
-
gap,
|
|
3072
|
-
-Math.hypot(x - centerX, y - centerY)
|
|
3073
|
-
];
|
|
3074
|
-
const k = best ? score.findIndex((value, index) => value !== best.score[index]) : 0;
|
|
3075
|
-
if (!best || k >= 0 && score[k] > best.score[k]) best = { x, y, score };
|
|
3076
|
-
}
|
|
3077
|
-
}
|
|
3078
|
-
return best && { x: best.x, y: best.y };
|
|
3079
|
-
}
|
|
3080
|
-
|
|
3081
3038
|
// src/pop-core-radial-layout.ts
|
|
3082
3039
|
var POP_WEDGE_SPAN_DEGREES = 80;
|
|
3083
3040
|
var POP_SUBTREE_OUTER_MARGIN = 24;
|
|
@@ -3244,11 +3201,10 @@ function getRadialPointOnCircle(angleDegrees, radius) {
|
|
|
3244
3201
|
const angleRad = angleDegrees * Math.PI / 180;
|
|
3245
3202
|
return { x: radius * Math.sin(angleRad), y: -radius * Math.cos(angleRad) };
|
|
3246
3203
|
}
|
|
3247
|
-
var LABEL_CHAR_WIDTH_RATIO = 0.6;
|
|
3248
3204
|
function getDotLabelGeometry(x, name, itemCount, itemCountRange, zoomScale) {
|
|
3249
3205
|
const dotRadius = calculateNodeDotRadius(itemCount, itemCountRange);
|
|
3250
3206
|
const fontSize = zoomScale === void 0 ? calculateNodeFontSize(itemCount, itemCountRange) : calculateLabelFontSizeAtZoom(itemCount, itemCountRange, zoomScale);
|
|
3251
|
-
const candidates =
|
|
3207
|
+
const candidates = (0, import_ui3.dotLabelCandidates)(name, dotRadius, fontSize, { gap: NODE_DOT_LABEL_GAP, preferRight: x >= 0 });
|
|
3252
3208
|
const { x: labelX, box: label } = candidates[0];
|
|
3253
3209
|
const dot = { x0: -dotRadius, x1: dotRadius, y0: -dotRadius, y1: dotRadius };
|
|
3254
3210
|
const halfHeight = Math.max(dotRadius, fontSize / 2);
|
|
@@ -3268,25 +3224,6 @@ function getDotLabelGeometry(x, name, itemCount, itemCountRange, zoomScale) {
|
|
|
3268
3224
|
candidates
|
|
3269
3225
|
};
|
|
3270
3226
|
}
|
|
3271
|
-
function getLabelCandidates(x, name, dotRadius, fontSize) {
|
|
3272
|
-
const width = name.length * fontSize * LABEL_CHAR_WIDTH_RATIO;
|
|
3273
|
-
const halfFont = fontSize / 2;
|
|
3274
|
-
const side = (right2) => {
|
|
3275
|
-
const labelX = (right2 ? 1 : -1) * (dotRadius + NODE_DOT_LABEL_GAP);
|
|
3276
|
-
return {
|
|
3277
|
-
x: labelX,
|
|
3278
|
-
y: 0,
|
|
3279
|
-
anchor: right2 ? "start" : "end",
|
|
3280
|
-
box: { x0: right2 ? labelX : labelX - width, x1: right2 ? labelX + width : labelX, y0: -halfFont, y1: halfFont }
|
|
3281
|
-
};
|
|
3282
|
-
};
|
|
3283
|
-
const stacked = (below) => {
|
|
3284
|
-
const y = (below ? 1 : -1) * (dotRadius + NODE_DOT_LABEL_GAP + halfFont);
|
|
3285
|
-
return { x: 0, y, anchor: "middle", box: { x0: -width / 2, x1: width / 2, y0: y - halfFont, y1: y + halfFont } };
|
|
3286
|
-
};
|
|
3287
|
-
const right = x >= 0;
|
|
3288
|
-
return [side(right), side(!right), stacked(false), stacked(true)];
|
|
3289
|
-
}
|
|
3290
3227
|
function renderRootLabels(container, roots) {
|
|
3291
3228
|
container.append("g").attr("class", "gtv-wheel-root-labels").selectAll("g.gtv-root-label").data(roots).join("g").attr("class", "gtv-root-label").attr("transform", (d) => `translate(${d.x}, ${d.y})`).append("text").attr("class", "gtv-node-label").attr("dominant-baseline", "middle").attr("fill", TEXT_COLOR).text((d) => d.name);
|
|
3292
3229
|
}
|
|
@@ -3442,10 +3379,10 @@ function hideCollidingLabels(d3Lib, container, itemCountRange, zoomScale) {
|
|
|
3442
3379
|
calculateLabelFontSizeAtZoom(d.itemCount, itemCountRange, zoomScale),
|
|
3443
3380
|
zoomScale
|
|
3444
3381
|
);
|
|
3445
|
-
return [d.id, { fontSize, candidates:
|
|
3382
|
+
return [d.id, { fontSize, candidates: (0, import_ui3.dotLabelCandidates)(d.name, d.radius, fontSize, { gap: NODE_DOT_LABEL_GAP, preferRight: d.x >= 0 }) }];
|
|
3446
3383
|
})
|
|
3447
3384
|
);
|
|
3448
|
-
const placement = placeLabels([
|
|
3385
|
+
const placement = (0, import_ui3.placeLabels)([
|
|
3449
3386
|
...roots.data().map((d) => ({
|
|
3450
3387
|
id: `root:${d.id}`,
|
|
3451
3388
|
labels: rootGeometryById.get(d.id).candidates.map((candidate) => offset(candidate.box, d.x, d.y)),
|
|
@@ -3590,7 +3527,6 @@ function WheelRadialCore({
|
|
|
3590
3527
|
defaultViewport,
|
|
3591
3528
|
viewportRestore,
|
|
3592
3529
|
onViewportChange,
|
|
3593
|
-
controlsOrientation = "vertical",
|
|
3594
3530
|
showZoomControls = "auto",
|
|
3595
3531
|
floatingControlsLeading,
|
|
3596
3532
|
busy,
|
|
@@ -3733,6 +3669,8 @@ function WheelRadialCore({
|
|
|
3733
3669
|
() => Math.max(chipClearanceFloor, chipClearanceFloor + maxCoreExtentDelta),
|
|
3734
3670
|
[chipClearanceFloor, maxCoreExtentDelta]
|
|
3735
3671
|
);
|
|
3672
|
+
const { viewportSize, minScale } = panZoom;
|
|
3673
|
+
const wheelCoverageRadius = viewportSize ? coverageRadius(viewportSize.width, viewportSize.height, minScale, wheelRadius) : null;
|
|
3736
3674
|
const onRootSelectRef = (0, import_react10.useRef)(onRootSelect);
|
|
3737
3675
|
(0, import_react10.useEffect)(() => {
|
|
3738
3676
|
onRootSelectRef.current = onRootSelect;
|
|
@@ -3884,6 +3822,7 @@ function WheelRadialCore({
|
|
|
3884
3822
|
].filter(Boolean).join(" "),
|
|
3885
3823
|
style: {
|
|
3886
3824
|
"--gtv-wheel-radius": `${wheelRadius}px`,
|
|
3825
|
+
...wheelCoverageRadius !== null && { "--gtv-wheel-coverage-radius": `${wheelCoverageRadius}px` },
|
|
3887
3826
|
"--gtv-wheel-rotation-transition-ms": `${WHEEL_ROTATION_TRANSITION_MS}ms`,
|
|
3888
3827
|
"--gtv-wheel-rotation-easing": WHEEL_ROTATION_EASING,
|
|
3889
3828
|
touchAction: panZoom.touchAction
|
|
@@ -4078,7 +4017,6 @@ function WheelRadialCore({
|
|
|
4078
4017
|
FloatingControls,
|
|
4079
4018
|
{
|
|
4080
4019
|
panZoom,
|
|
4081
|
-
orientation: controlsOrientation,
|
|
4082
4020
|
zoomControls: showZoomControls,
|
|
4083
4021
|
leading: floatingControlsLeading,
|
|
4084
4022
|
busy,
|
|
@@ -4098,6 +4036,7 @@ function GenreTreeWheelRadial(props) {
|
|
|
4098
4036
|
|
|
4099
4037
|
// src/GenreTreeWheelRadialPopCoreBase.tsx
|
|
4100
4038
|
var import_react13 = require("react");
|
|
4039
|
+
var import_ui4 = require("@behindthemusictree/ui");
|
|
4101
4040
|
var d34 = __toESM(require("d3"), 1);
|
|
4102
4041
|
|
|
4103
4042
|
// src/use-focus-history.ts
|
|
@@ -4276,7 +4215,6 @@ function WheelRadialPopCoreCore({
|
|
|
4276
4215
|
defaultViewport,
|
|
4277
4216
|
viewportRestore,
|
|
4278
4217
|
onViewportChange,
|
|
4279
|
-
controlsOrientation = "vertical",
|
|
4280
4218
|
showZoomControls = "auto",
|
|
4281
4219
|
floatingControlsLeading,
|
|
4282
4220
|
busy,
|
|
@@ -4602,6 +4540,12 @@ function WheelRadialPopCoreCore({
|
|
|
4602
4540
|
() => coreRootCircleRadius + maxCoreExtentDelta,
|
|
4603
4541
|
[coreRootCircleRadius, maxCoreExtentDelta]
|
|
4604
4542
|
);
|
|
4543
|
+
const { viewportSize, minScale } = panZoom;
|
|
4544
|
+
const wheelCoverageRadius = viewportSize ? coverageRadius(viewportSize.width, viewportSize.height, minScale, svgCanvasRadius) : null;
|
|
4545
|
+
const wheelCoverageRadiusRef = (0, import_react13.useRef)(wheelCoverageRadius);
|
|
4546
|
+
(0, import_react13.useEffect)(() => {
|
|
4547
|
+
wheelCoverageRadiusRef.current = wheelCoverageRadius;
|
|
4548
|
+
}, [wheelCoverageRadius]);
|
|
4605
4549
|
const middleCircleFloor = mainstreamCircleRadius;
|
|
4606
4550
|
const onRootSelectRef = (0, import_react13.useRef)(onRootSelect);
|
|
4607
4551
|
(0, import_react13.useEffect)(() => {
|
|
@@ -4715,13 +4659,14 @@ function WheelRadialPopCoreCore({
|
|
|
4715
4659
|
const next = at >= 0 ? siblings[at + 1] : void 0;
|
|
4716
4660
|
const startDeg = prev ? (subtreeRange(prev).max + mine.min) / 2 : angle - span / 2;
|
|
4717
4661
|
const endDeg = next ? (mine.max + subtreeRange(next).min) / 2 : angle + span / 2;
|
|
4718
|
-
const
|
|
4662
|
+
const startAngle = startDeg * Math.PI / 180;
|
|
4663
|
+
const endAngle = endDeg * Math.PI / 180;
|
|
4664
|
+
sectorGroup.append("path").attr("class", "gtv-wheel-scene-sector").attr("data-start-angle", startAngle).attr("data-end-angle", endAngle).attr("d", d34.arc()({
|
|
4719
4665
|
innerRadius: coreRootCircleRadius,
|
|
4720
|
-
outerRadius:
|
|
4721
|
-
startAngle
|
|
4722
|
-
endAngle
|
|
4723
|
-
});
|
|
4724
|
-
sectorGroup.append("path").attr("class", "gtv-wheel-scene-sector").attr("d", arcPath).attr("fill", shadeSurface(getGenreNodeColor(index, rootId), UNDERGROUND_DARKEN_RATIO)).attr("fill-opacity", UNDERGROUND_SECTOR_FILL_OPACITY);
|
|
4666
|
+
outerRadius: wheelCoverageRadiusRef.current ?? svgCanvasRadius,
|
|
4667
|
+
startAngle,
|
|
4668
|
+
endAngle
|
|
4669
|
+
})).attr("fill", shadeSurface(getGenreNodeColor(index, rootId), UNDERGROUND_DARKEN_RATIO)).attr("fill-opacity", UNDERGROUND_SECTOR_FILL_OPACITY);
|
|
4725
4670
|
});
|
|
4726
4671
|
const branchGroup = sectorGroup.append("g").attr("class", "gtv-wheel-core-branch").attr("data-gtv-branch-index", branchIndex);
|
|
4727
4672
|
renderPopSubtree(
|
|
@@ -4891,6 +4836,17 @@ function WheelRadialPopCoreCore({
|
|
|
4891
4836
|
wheelItemCountRange,
|
|
4892
4837
|
panel?.node.id
|
|
4893
4838
|
]);
|
|
4839
|
+
(0, import_react13.useEffect)(() => {
|
|
4840
|
+
if (!popSvgRef.current || wheelCoverageRadius === null) return;
|
|
4841
|
+
d34.select(popSvgRef.current).selectAll("path.gtv-wheel-scene-sector").attr("d", function() {
|
|
4842
|
+
return d34.arc()({
|
|
4843
|
+
innerRadius: coreRootCircleRadius,
|
|
4844
|
+
outerRadius: wheelCoverageRadius,
|
|
4845
|
+
startAngle: Number(this.getAttribute("data-start-angle")),
|
|
4846
|
+
endAngle: Number(this.getAttribute("data-end-angle"))
|
|
4847
|
+
});
|
|
4848
|
+
});
|
|
4849
|
+
}, [wheelCoverageRadius, coreRootCircleRadius]);
|
|
4894
4850
|
(0, import_react13.useEffect)(() => {
|
|
4895
4851
|
const timeout = setTimeout(() => {
|
|
4896
4852
|
if (popSvgRef.current) hideCollidingLabels(d34, d34.select(popSvgRef.current), wheelItemCountRange, zoomScale);
|
|
@@ -4933,7 +4889,7 @@ function WheelRadialPopCoreCore({
|
|
|
4933
4889
|
const inset = REGION_LABEL_TARGET_PX / 2;
|
|
4934
4890
|
const spot = pickEmptyLabelSpot(
|
|
4935
4891
|
{ x0: vp.left + inset, y0: vp.top + inset, x1: vp.right - inset, y1: vp.bottom - inset },
|
|
4936
|
-
name.length * REGION_LABEL_TARGET_PX * LABEL_CHAR_WIDTH_RATIO,
|
|
4892
|
+
name.length * REGION_LABEL_TARGET_PX * import_ui4.LABEL_CHAR_WIDTH_RATIO,
|
|
4937
4893
|
REGION_LABEL_TARGET_PX,
|
|
4938
4894
|
obstacles,
|
|
4939
4895
|
(x, y) => rootIdAt(x, y) === rootId
|
|
@@ -5028,6 +4984,7 @@ function WheelRadialPopCoreCore({
|
|
|
5028
4984
|
style: {
|
|
5029
4985
|
"--gtv-wheel-radius": `${coreRootCircleRadius}px`,
|
|
5030
4986
|
"--gtv-wheel-svg-radius": `${svgCanvasRadius}px`,
|
|
4987
|
+
...wheelCoverageRadius !== null && { "--gtv-wheel-coverage-radius": `${wheelCoverageRadius}px` },
|
|
5031
4988
|
"--gtv-wheel-rotation-transition-ms": `${WHEEL_ROTATION_TRANSITION_MS}ms`,
|
|
5032
4989
|
"--gtv-wheel-rotation-easing": WHEEL_ROTATION_EASING,
|
|
5033
4990
|
touchAction: panZoom.touchAction
|
|
@@ -5282,7 +5239,6 @@ function WheelRadialPopCoreCore({
|
|
|
5282
5239
|
FloatingControls,
|
|
5283
5240
|
{
|
|
5284
5241
|
panZoom,
|
|
5285
|
-
orientation: controlsOrientation,
|
|
5286
5242
|
zoomControls: showZoomControls,
|
|
5287
5243
|
leading: floatingControlsLeading,
|
|
5288
5244
|
busy,
|
|
@@ -6069,6 +6025,7 @@ function GenreTreeViewSkeleton({ viewMode }) {
|
|
|
6069
6025
|
}
|
|
6070
6026
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6071
6027
|
0 && (module.exports = {
|
|
6028
|
+
ACCENT_TEXT_COLOR,
|
|
6072
6029
|
DEFAULT_FRAME_HEIGHT,
|
|
6073
6030
|
DEFAULT_FRAME_WIDTH,
|
|
6074
6031
|
GenreTree,
|
|
@@ -6081,8 +6038,12 @@ function GenreTreeViewSkeleton({ viewMode }) {
|
|
|
6081
6038
|
GenreTreeWheelRadialPopCore,
|
|
6082
6039
|
GenreTreeWheelRight,
|
|
6083
6040
|
GenreTreeWheelSkeleton,
|
|
6041
|
+
InfoPanel,
|
|
6042
|
+
ROOT_TINT_RATIO,
|
|
6043
|
+
TEXT_COLOR,
|
|
6084
6044
|
getGenreTreeColor,
|
|
6085
6045
|
groupNodesByRoot,
|
|
6086
|
-
isZoomableNode
|
|
6046
|
+
isZoomableNode,
|
|
6047
|
+
tintSurface
|
|
6087
6048
|
});
|
|
6088
6049
|
//# sourceMappingURL=index.cjs.map
|