Skip to content

Commit f00fa5d

Browse files
temp commit
1 parent 79bb291 commit f00fa5d

File tree

2 files changed

+117
-28
lines changed

2 files changed

+117
-28
lines changed

src/symbols/nodes/Badge.tsx

Lines changed: 87 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ import { Color } from 'three';
77
import type { NodeRendererProps } from '../../types';
88
import { animationConfig } from '../../utils';
99

10-
// Layout constants
11-
const CHAR_WIDTH_ESTIMATE = 0.15;
12-
const ICON_TEXT_GAP = 0.15;
13-
1410
export type BadgePosition =
1511
| 'top-right'
1612
| 'top-left'
@@ -20,7 +16,7 @@ export type BadgePosition =
2016

2117
export type IconPosition = 'start' | 'end';
2218

23-
export interface BadgeProps extends NodeRendererProps {
19+
export interface BadgeProps extends Omit<NodeRendererProps, 'opacity'> {
2420
/**
2521
* The text to display in the badge.
2622
*/
@@ -31,6 +27,12 @@ export interface BadgeProps extends NodeRendererProps {
3127
*/
3228
backgroundColor?: string;
3329

30+
/**
31+
* Opacity of the badge background and stroke (0-1).
32+
* Default: 1
33+
*/
34+
opacity?: number;
35+
3436
/**
3537
* Text color of the badge.
3638
*/
@@ -84,6 +86,30 @@ export interface BadgeProps extends NodeRendererProps {
8486
* the text remains centered and only the icon moves to the specified position.
8587
*/
8688
iconPosition?: IconPosition | [number, number];
89+
90+
/**
91+
* Font size for the badge text.
92+
*/
93+
fontSize?: number;
94+
95+
/**
96+
* Font weight for the badge text (100-900).
97+
* Values outside this range will be clamped to the nearest valid value.
98+
* Common values: 400 (normal), 700 (bold), 900 (extra bold).
99+
*/
100+
fontWeight?: number;
101+
102+
/**
103+
* Character width estimate for calculating text width.
104+
* Default: 0.2
105+
*/
106+
charWidthEstimate?: number;
107+
108+
/**
109+
* Gap between icon and text.
110+
* Default: 0.01
111+
*/
112+
iconTextGap?: number;
87113
}
88114

89115
export const Badge: FC<BadgeProps> = ({
@@ -98,10 +124,14 @@ export const Badge: FC<BadgeProps> = ({
98124
radius = 0.12,
99125
badgeSize = 1.5,
100126
position = 'top-right',
101-
padding = 0.3,
127+
padding = 0.15,
102128
icon,
103129
iconSize = 0.35,
104-
iconPosition = 'start'
130+
iconPosition = 'start',
131+
fontSize = 0.3,
132+
fontWeight,
133+
charWidthEstimate = 0.2,
134+
iconTextGap = 0.01
105135
}) => {
106136
const normalizedBgColor = useMemo(
107137
() => new Color(backgroundColor),
@@ -115,6 +145,13 @@ export const Badge: FC<BadgeProps> = ({
115145
// Guard for radius
116146
const normalizedRadius = Math.min(radius, 0.2);
117147

148+
// Normalize fontWeight to valid CSS range (100-900)
149+
const normalizedFontWeight = useMemo(() => {
150+
if (fontWeight === undefined) return undefined;
151+
// Clamp numeric values to 100-900 range
152+
return Math.max(100, Math.min(900, fontWeight));
153+
}, [fontWeight]);
154+
118155
// Calculate position based on preset or custom coordinates
119156
const badgePosition = useMemo((): [number, number, number] => {
120157
if (Array.isArray(position)) {
@@ -138,37 +175,58 @@ export const Badge: FC<BadgeProps> = ({
138175
}
139176
}, [position, size]);
140177

178+
// Shared text size calculations (used by both badgeDimensions and contentLayout)
179+
const textSizeCalculations = useMemo(() => {
180+
const fontSizeScale = fontSize / 0.3;
181+
const fontWeightMultiplier =
182+
normalizedFontWeight && normalizedFontWeight >= 700 ? 1.1 : 1;
183+
const adjustedCharWidth =
184+
charWidthEstimate * fontSizeScale * fontWeightMultiplier;
185+
const estimatedTextWidth = label.length * adjustedCharWidth;
186+
187+
return {
188+
fontSizeScale,
189+
fontWeightMultiplier,
190+
adjustedCharWidth,
191+
estimatedTextWidth
192+
};
193+
}, [fontSize, normalizedFontWeight, charWidthEstimate, label.length]);
194+
141195
// Calculate dynamic badge dimensions based on text length and icon
142196
const badgeDimensions = useMemo(() => {
143197
const baseWidth = 0.5;
144198
const baseHeight = 0.5;
145199
const minWidth = baseWidth;
146200
const minHeight = baseHeight;
147201

148-
// Estimate text width based on character count
149-
const charCount = label.length;
150-
let estimatedWidth = Math.max(
151-
minWidth,
152-
Math.min(charCount * CHAR_WIDTH_ESTIMATE + padding, 2.0 + padding)
153-
); // Add padding to width
202+
const { fontSizeScale, estimatedTextWidth } = textSizeCalculations;
154203

155-
// Add icon width if icon is present
204+
// Calculate content width (text + icon + gap, no padding yet)
205+
let contentWidth = estimatedTextWidth;
156206
if (icon) {
157-
estimatedWidth += iconSize;
207+
contentWidth += iconSize + iconTextGap;
158208
}
159209

210+
// Add padding to total width (padding on both left and right sides)
211+
const estimatedWidth = Math.max(minWidth, contentWidth + padding * 2);
212+
213+
// Scale height based on fontSize
214+
const charCount = label.length;
160215
const estimatedHeight = Math.max(
161216
minHeight,
162-
Math.min(charCount * 0.05 + padding * 0.5, 0.8 + padding * 0.5)
163-
); // Add padding to height
217+
Math.min(
218+
charCount * 0.05 * fontSizeScale + padding * 0.5,
219+
0.8 * fontSizeScale + padding * 0.5
220+
)
221+
);
164222

165223
return {
166224
width: estimatedWidth,
167225
height: estimatedHeight
168226
};
169-
}, [label, padding, icon, iconSize]);
227+
}, [textSizeCalculations, label.length, padding, icon, iconSize, iconTextGap]);
170228

171-
const { scale, badgeOpacity } = useSpring({
229+
const { scale } = useSpring({
172230
from: {
173231
scale: [0.00001, 0.00001, 0.00001],
174232
badgeOpacity: 0
@@ -204,26 +262,26 @@ export const Badge: FC<BadgeProps> = ({
204262
};
205263
}
206264

207-
const estimatedTextWidth = label.length * CHAR_WIDTH_ESTIMATE;
208-
const totalContentWidth = iconSize + estimatedTextWidth;
265+
const { estimatedTextWidth } = textSizeCalculations;
266+
const totalContentWidth = iconSize + iconTextGap + estimatedTextWidth;
209267
const startX = -totalContentWidth / 2;
210268

211269
if (iconPosition === 'start') {
212270
return {
213-
iconX: startX + iconSize - 0.5 / 2,
271+
iconX: startX + iconSize / 2,
214272
iconY: 0,
215-
textX: startX + iconSize + estimatedTextWidth / 2,
273+
textX: startX + iconSize + iconTextGap + estimatedTextWidth / 2,
216274
textY: 0
217275
};
218276
} else {
219277
return {
220278
textX: startX + estimatedTextWidth / 2,
221279
textY: 0,
222-
iconX: startX + estimatedTextWidth + ICON_TEXT_GAP + iconSize / 2,
280+
iconX: startX + estimatedTextWidth + iconTextGap + iconSize / 2,
223281
iconY: 0
224282
};
225283
}
226-
}, [icon, iconSize, iconPosition, label.length]);
284+
}, [textSizeCalculations, icon, iconSize, iconPosition, iconTextGap]);
227285

228286
return (
229287
<Billboard position={badgePosition}>
@@ -241,6 +299,7 @@ export const Badge: FC<BadgeProps> = ({
241299
smoothness={8}
242300
material-color={normalizedStrokeColor}
243301
material-transparent={true}
302+
material-opacity={opacity}
244303
/>
245304
</a.mesh>
246305
)}
@@ -252,6 +311,7 @@ export const Badge: FC<BadgeProps> = ({
252311
smoothness={8}
253312
material-color={normalizedBgColor}
254313
material-transparent={true}
314+
material-opacity={opacity}
255315
/>
256316
</a.mesh>
257317
{/* Icon */}
@@ -268,11 +328,11 @@ export const Badge: FC<BadgeProps> = ({
268328
{/* Text */}
269329
<Text
270330
position={[contentLayout.textX, contentLayout.textY, 1.1]}
271-
fontSize={0.3}
331+
fontSize={fontSize}
332+
fontWeight={normalizedFontWeight}
272333
color={normalizedTextColor}
273334
anchorX="center"
274335
anchorY="middle"
275-
maxWidth={badgeDimensions.width - 0.2}
276336
textAlign="center"
277337
material-depthTest={false}
278338
material-depthWrite={false}

stories/demos/Badge.story.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ export const WithIconEnd = () => (
121121
icon={userSvg}
122122
iconSize={0.3}
123123
iconPosition="end"
124-
padding={0.5}
125124
strokeColor="#059669"
126125
strokeWidth={0.1}
127126
radius={0.15}
@@ -150,10 +149,40 @@ export const WithCustomIconPosition = () => (
150149
iconSize={0.6}
151150
iconPosition={[0.7, 0.3]}
152151
strokeWidth={0.2}
152+
padding={0}
153153
radius={0.15}
154154
position="bottom-right"
155155
/>
156156
</group>
157157
)}
158158
/>
159159
);
160+
161+
export const WithCustomTextAndColor = () => (
162+
<GraphCanvas
163+
nodes={simpleNodes}
164+
edges={simpleEdges}
165+
cameraMode="rotate"
166+
renderNode={({ node, ...rest }) => (
167+
<group>
168+
<Sphere {...rest} node={node} />
169+
<Badge
170+
{...rest}
171+
node={node}
172+
label="User"
173+
backgroundColor="#f59e0b"
174+
strokeColor="red"
175+
textColor="#ffffff"
176+
icon={userSvg}
177+
iconSize={0.35}
178+
padding={0.1}
179+
radius={0.3}
180+
iconPosition="start"
181+
position={[0, -10, 0]}
182+
opacity={0.6}
183+
strokeWidth={0.03}
184+
/>
185+
</group>
186+
)}
187+
/>
188+
);

0 commit comments

Comments
 (0)