Skip to content

Commit 652c2c8

Browse files
committed
feat: text-metrics
1 parent c97f01f commit 652c2c8

File tree

5 files changed

+56
-14
lines changed

5 files changed

+56
-14
lines changed

index.d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5590,6 +5590,20 @@ declare namespace cytoscape {
55905590
* height of a line of text.
55915591
*/
55925592
"line-height": PropertyValue<SingularType, number>;
5593+
/**
5594+
* Horizontal and vertical margin of error applied to a node's bounding box.
5595+
* These margins help compensate for layout inaccuracies or rendering quirks.
5596+
* Values are in pixels.
5597+
*/
5598+
"bbox-margin-error-x": PropertyValue<SingularType, number>;
5599+
"bbox-margin-error-y": PropertyValue<SingularType, number>;
5600+
/**
5601+
* The vertical alignment of text relative to its baseline. This affects how text
5602+
* dimensions are calculated and displayed.
5603+
* - `default`: Calculate using font size.
5604+
* - `actual`: Calculate using actual label text metrics (for ascending/descending character size).
5605+
*/
5606+
"text-metrics": PropertyValue<SingularType, "default" | "actual">;
55935607

55945608
/**
55955609
* Node label alignment:

src/collection/dimensions/bounds.mjs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,8 @@ let updateBoundsFromLabel = function( bounds, ele, prefix ){
291291
let borderWidth = ele.pstyle( 'text-border-width' ).pfValue;
292292
let halfBorderWidth = borderWidth / 2;
293293
let padding = ele.pstyle( 'text-background-padding' ).pfValue;
294-
let marginOfError = 2; // expand to work around browser dimension inaccuracies
294+
let marginOfErrorX = ele.pstyle( 'bbox-margin-error-x' ).pfValue; // expand to work around browser dimension inaccuracies
295+
let marginOfErrorY = ele.pstyle( 'bbox-margin-error-y' ).pfValue; // expand to work around browser dimension inaccuracies
295296

296297
let lh = labelHeight;
297298
let lw = labelWidth;
@@ -341,10 +342,10 @@ let updateBoundsFromLabel = function( bounds, ele, prefix ){
341342
}
342343

343344
// shift by margin and expand by outline and border
344-
let leftPad = marginX - Math.max( outlineWidth, halfBorderWidth ) - padding - marginOfError;
345-
let rightPad = marginX + Math.max( outlineWidth, halfBorderWidth ) + padding + marginOfError;
346-
let topPad = marginY - Math.max( outlineWidth, halfBorderWidth ) - padding - marginOfError;
347-
let botPad = marginY + Math.max( outlineWidth, halfBorderWidth ) + padding + marginOfError;
345+
let leftPad = marginX - Math.max( outlineWidth, halfBorderWidth ) - padding - marginOfErrorX;
346+
let rightPad = marginX + Math.max( outlineWidth, halfBorderWidth ) + padding + marginOfErrorX;
347+
let topPad = marginY - Math.max( outlineWidth, halfBorderWidth ) - padding - marginOfErrorY;
348+
let botPad = marginY + Math.max( outlineWidth, halfBorderWidth ) + padding + marginOfErrorY;
348349

349350
lx1 += leftPad;
350351
lx2 += rightPad;

src/extensions/renderer/base/coord-ele-math/labels.mjs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -312,14 +312,14 @@ BRp.applyPrefixedLabelDimensions = function( ele, prefix ){
312312

313313
let labelDims = this.calculateLabelDimensions( ele, text );
314314
let lineHeight = ele.pstyle('line-height').pfValue;
315+
let size = ele.pstyle('font-size').pfValue;
315316
let textWrap = ele.pstyle('text-wrap').strValue;
316317
let lines = util.getPrefixedProperty( _p.rscratch, 'labelWrapCachedLines', prefix ) || [];
317318
let numLines = textWrap !== 'wrap' ? 1 : Math.max(lines.length, 1);
318-
let normPerLineHeight = labelDims.height / numLines;
319-
let labelLineHeight = normPerLineHeight * lineHeight;
319+
let labelLineHeight = size * lineHeight;
320320

321321
let width = labelDims.width;
322-
let height = labelDims.height + (numLines - 1) * (lineHeight - 1) * normPerLineHeight;
322+
let height = labelDims.height + (numLines - 1) * (lineHeight - 1) * size;
323323

324324
util.setPrefixedProperty( _p.rstyle, 'labelWidth', prefix, width );
325325
util.setPrefixedProperty( _p.rscratch, 'labelWidth', prefix, width );
@@ -328,6 +328,7 @@ BRp.applyPrefixedLabelDimensions = function( ele, prefix ){
328328
util.setPrefixedProperty( _p.rscratch, 'labelHeight', prefix, height );
329329

330330
util.setPrefixedProperty( _p.rscratch, 'labelLineHeight', prefix, labelLineHeight );
331+
util.setPrefixedProperty( _p.rscratch, 'labelActualDescent', prefix, labelDims.labelActualDescent );
331332
};
332333

333334
BRp.getLabelText = function( ele, prefix ){
@@ -487,6 +488,7 @@ BRp.calculateLabelDimensions = function( ele, text ){
487488
let size = ele.pstyle('font-size').pfValue;
488489
let family = ele.pstyle('font-family').strValue;
489490
let weight = ele.pstyle('font-weight').strValue;
491+
let textMetrics = ele.pstyle('text-metrics').strValue || "default";
490492

491493
let canvas = this.labelCalcCanvas;
492494
let c2d = this.labelCalcCanvasContext;
@@ -509,23 +511,38 @@ BRp.calculateLabelDimensions = function( ele, text ){
509511
let width = 0;
510512
let height = 0;
511513
let lines = text.split('\n');
514+
let lineCount = lines.length;
515+
let labelActualDescent = 0;
516+
let labelActualAscent = 0;
512517

513-
for( let i = 0; i < lines.length; i++ ){
518+
for( let i = 0; i < lineCount; i++ ){
514519
let line = lines[i];
515520
let metrics = c2d.measureText(line);
516521
let w = Math.ceil(metrics.width);
517522
let h = size;
523+
if (textMetrics === "actual") {
524+
if (i === 0) {
525+
labelActualAscent = metrics.actualBoundingBoxAscent;
526+
}
527+
if (i === lineCount - 1) {
528+
labelActualDescent = metrics.actualBoundingBoxDescent;
529+
}
530+
}
518531

519532
width = Math.max(w, width);
520533
height += h;
521534
}
522-
535+
if (textMetrics === "actual") {
536+
height -= size - labelActualAscent - labelActualDescent;
537+
}
523538
width += padding;
524539
height += padding;
525540

526541
return {
527542
width,
528-
height
543+
height,
544+
labelActualAscent,
545+
labelActualDescent
529546
};
530547
};
531548

src/extensions/renderer/canvas/drawing-label-text.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ CRp.drawElementText = function( context, ele, shiftToOriginWithBb, force, prefix
3737
if( !label || !label.value ){ return; }
3838

3939
let justification = r.getLabelJustification(ele);
40+
let isTextMetricsActual = ele.pstyle( 'text-metrics' ).strValue === 'actual';
4041

4142
context.textAlign = justification;
42-
context.textBaseline = 'bottom';
43+
context.textBaseline = isTextMetricsActual ? 'alphabetic' : 'bottom';
4344
} else {
4445
let badLine = ele.element()._private.rscratch.badLine;
4546
let label = ele.pstyle( 'label' );
@@ -198,6 +199,7 @@ CRp.drawText = function( context, ele, prefix, applyRotation = true, useEleOpaci
198199
let pdash = prefix ? prefix + '-' : '';
199200
let textW = util.getPrefixedProperty( rscratch, 'labelWidth', prefix );
200201
let textH = util.getPrefixedProperty( rscratch, 'labelHeight', prefix );
202+
let labelActualDescent = util.getPrefixedProperty( rscratch, 'labelActualDescent', prefix );
201203
let marginX = ele.pstyle( pdash + 'text-margin-x' ).pfValue;
202204
let marginY = ele.pstyle( pdash + 'text-margin-y' ).pfValue;
203205

@@ -335,7 +337,8 @@ CRp.drawText = function( context, ele, prefix, applyRotation = true, useEleOpaci
335337
if( lineWidth > 0 ){
336338
context.lineWidth = lineWidth;
337339
}
338-
340+
341+
textY -= labelActualDescent;
339342
if( ele.pstyle( 'text-wrap' ).value === 'wrap' ){
340343
let lines = util.getPrefixedProperty( rscratch, 'labelWrapCachedLines', prefix );
341344
let lineHeight = util.getPrefixedProperty( rscratch, 'labelLineHeight', prefix );

src/style/properties.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ const styfn = {};
9191
valign: { enums: [ 'top', 'center', 'bottom' ] },
9292
halign: { enums: [ 'left', 'center', 'right' ] },
9393
justification: { enums: [ 'left', 'center', 'right', 'auto' ] },
94+
textMetrics: { enums: [ 'default', 'actual' ] },
9495
text: { string: true },
9596
data: { mapping: true, regex: data( 'data' ) },
9697
layoutData: { mapping: true, regex: data( 'layoutData' ) },
@@ -217,7 +218,9 @@ const styfn = {};
217218
{ name: 'text-overflow-wrap', type: t.textOverflowWrap, triggersBounds: diff.any },
218219
{ name: 'text-max-width', type: t.size, triggersBounds: diff.any },
219220
{ name: 'text-outline-width', type: t.size, triggersBounds: diff.any },
220-
{ name: 'line-height', type: t.positiveNumber, triggersBounds: diff.any }
221+
{ name: 'line-height', type: t.positiveNumber, triggersBounds: diff.any },
222+
{ name: 'bbox-margin-error-x', type: t.size, triggersBounds: diff.any },
223+
{ name: 'bbox-margin-error-y', type: t.size, triggersBounds: diff.any },
221224
];
222225

223226
let commonLabel = [
@@ -235,6 +238,7 @@ const styfn = {};
235238
{ name: 'text-border-style', type: t.borderStyle, triggersBounds: diff.any },
236239
{ name: 'text-background-shape', type: t.textBackgroundShape, triggersBounds: diff.any },
237240
{ name: 'text-justification', type: t.justification },
241+
{ name: 'text-metrics', type: t.textMetrics },
238242
{ name: 'box-select-labels', type: t.bool, triggersBounds: diff.any },
239243
];
240244

@@ -671,6 +675,9 @@ styfn.getDefaultProperties = function(){
671675
'underlay-padding': 10,
672676
'underlay-shape': 'round-rectangle',
673677
'underlay-corner-radius': 'auto',
678+
'bbox-margin-error-x': 2,
679+
'bbox-margin-error-y': 2,
680+
'text-metrics': 'default',
674681
'transition-property': 'none',
675682
'transition-duration': 0,
676683
'transition-delay': 0,

0 commit comments

Comments
 (0)