@ifc-lite/drawing-2d 2.1.1 → 3.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.
Files changed (48) hide show
  1. package/README.md +2 -3
  2. package/dist/dxf/convert.d.ts.map +1 -1
  3. package/dist/dxf/convert.js +16 -0
  4. package/dist/dxf/convert.js.map +1 -1
  5. package/dist/graphic-overrides/ifc-type-hierarchy.d.ts +38 -0
  6. package/dist/graphic-overrides/ifc-type-hierarchy.d.ts.map +1 -0
  7. package/dist/graphic-overrides/ifc-type-hierarchy.js +156 -0
  8. package/dist/graphic-overrides/ifc-type-hierarchy.js.map +1 -0
  9. package/dist/graphic-overrides/rule-engine.d.ts.map +1 -1
  10. package/dist/graphic-overrides/rule-engine.js +1 -34
  11. package/dist/graphic-overrides/rule-engine.js.map +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/openings/opening-filter.d.ts +10 -1
  17. package/dist/openings/opening-filter.d.ts.map +1 -1
  18. package/dist/openings/opening-filter.js.map +1 -1
  19. package/dist/sheet/index.d.ts +2 -4
  20. package/dist/sheet/index.d.ts.map +1 -1
  21. package/dist/sheet/index.js +1 -2
  22. package/dist/sheet/index.js.map +1 -1
  23. package/dist/sheet/scale-bar-types.d.ts +66 -36
  24. package/dist/sheet/scale-bar-types.d.ts.map +1 -1
  25. package/dist/sheet/scale-bar-types.js +79 -12
  26. package/dist/sheet/scale-bar-types.js.map +1 -1
  27. package/dist/sheet/sheet-types.d.ts +53 -0
  28. package/dist/sheet/sheet-types.d.ts.map +1 -1
  29. package/dist/sheet/sheet-types.js +53 -0
  30. package/dist/sheet/sheet-types.js.map +1 -1
  31. package/dist/sheet/title-block-renderer.d.ts.map +1 -1
  32. package/dist/sheet/title-block-renderer.js +111 -11
  33. package/dist/sheet/title-block-renderer.js.map +1 -1
  34. package/dist/styles.d.ts +22 -3
  35. package/dist/styles.d.ts.map +1 -1
  36. package/dist/styles.js +39 -8
  37. package/dist/styles.js.map +1 -1
  38. package/dist/svg-exporter.d.ts +9 -1
  39. package/dist/svg-exporter.d.ts.map +1 -1
  40. package/dist/svg-exporter.js.map +1 -1
  41. package/dist/symbols/door-symbol.d.ts +9 -1
  42. package/dist/symbols/door-symbol.d.ts.map +1 -1
  43. package/dist/symbols/door-symbol.js.map +1 -1
  44. package/package.json +5 -3
  45. package/dist/sheet/scale-bar-renderer.d.ts +0 -29
  46. package/dist/sheet/scale-bar-renderer.d.ts.map +0 -1
  47. package/dist/sheet/scale-bar-renderer.js +0 -287
  48. package/dist/sheet/scale-bar-renderer.js.map +0 -1
@@ -13,16 +13,14 @@ export type { FrameStyle, FrameBorderConfig, FrameMargins, DrawingFrame, } from
13
13
  export { FRAME_PRESETS, createFrame, getDefaultFrame, } from './frame-types.js';
14
14
  export type { TitleBlockPosition, TitleBlockLayout, TitleBlockField, TitleBlockLogo, RevisionEntry, TitleBlockConfig, } from './title-block-types.js';
15
15
  export { DEFAULT_TITLE_BLOCK_FIELDS, TITLE_BLOCK_PRESETS, createTitleBlock, getDefaultTitleBlock, updateTitleBlockField, } from './title-block-types.js';
16
- export type { ScaleBarStyle, ScaleBarPosition, ScaleBarUnits, ScaleBarConfig, NorthArrowStyle, NorthArrowConfig, } from './scale-bar-types.js';
16
+ export type { ScaleBarConfig, NorthArrowStyle, NorthArrowConfig, } from './scale-bar-types.js';
17
17
  export { DEFAULT_SCALE_BAR, DEFAULT_NORTH_ARROW, calculateOptimalScaleBarLength, calculateOptimalDivisions, } from './scale-bar-types.js';
18
18
  export type { ScaleStampRect, ScaleStampText, ScaleStampBar, ScaleStamp, StampedSheetLayout, } from './scale-stamp.js';
19
19
  export { addScaleStamp } from './scale-stamp.js';
20
20
  export type { ViewportBounds, DrawingSheet, SheetCreationOptions, } from './sheet-types.js';
21
- export { calculateViewportBounds, calculateDrawingTransform, } from './sheet-types.js';
21
+ export { calculateViewportBounds, calculateDrawingTransform, calculateDrawingTransformForAxis, } from './sheet-types.js';
22
22
  export type { FrameRenderResult, } from './frame-renderer.js';
23
23
  export { renderFrame } from './frame-renderer.js';
24
24
  export type { FrameInnerBounds, TitleBlockRenderResult, TitleBlockExtras, } from './title-block-renderer.js';
25
25
  export { renderTitleBlock } from './title-block-renderer.js';
26
- export type { PositionMm } from './scale-bar-renderer.js';
27
- export { renderScaleBar, renderNorthArrow } from './scale-bar-renderer.js';
28
26
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sheet/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAGH,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EACV,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,YAAY,GACb,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,WAAW,EACX,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,aAAa,EACb,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAGhC,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,eAAe,EACf,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAK9B,YAAY,EACV,cAAc,EACd,cAAc,EACd,aAAa,EACb,UAAU,EACV,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGjD,YAAY,EACV,cAAc,EACd,YAAY,EACZ,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EACV,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAE7D,YAAY,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sheet/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAGH,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EACV,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,YAAY,GACb,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,WAAW,EACX,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,aAAa,EACb,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAGhC,YAAY,EACV,cAAc,EACd,eAAe,EACf,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAK9B,YAAY,EACV,cAAc,EACd,cAAc,EACd,aAAa,EACb,UAAU,EACV,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGjD,YAAY,EACV,cAAc,EACd,YAAY,EACZ,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,uBAAuB,EACvB,yBAAyB,EACzB,gCAAgC,GACjC,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EACV,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC"}
@@ -6,8 +6,7 @@ export { FRAME_PRESETS, createFrame, getDefaultFrame, } from './frame-types.js';
6
6
  export { DEFAULT_TITLE_BLOCK_FIELDS, TITLE_BLOCK_PRESETS, createTitleBlock, getDefaultTitleBlock, updateTitleBlockField, } from './title-block-types.js';
7
7
  export { DEFAULT_SCALE_BAR, DEFAULT_NORTH_ARROW, calculateOptimalScaleBarLength, calculateOptimalDivisions, } from './scale-bar-types.js';
8
8
  export { addScaleStamp } from './scale-stamp.js';
9
- export { calculateViewportBounds, calculateDrawingTransform, } from './sheet-types.js';
9
+ export { calculateViewportBounds, calculateDrawingTransform, calculateDrawingTransformForAxis, } from './sheet-types.js';
10
10
  export { renderFrame } from './frame-renderer.js';
11
11
  export { renderTitleBlock } from './title-block-renderer.js';
12
- export { renderScaleBar, renderNorthArrow } from './scale-bar-renderer.js';
13
12
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sheet/index.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAkB/D,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EACL,aAAa,EACb,WAAW,EACX,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAW1B,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAWhC,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAY9B,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAQjD,OAAO,EACL,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,kBAAkB,CAAC;AAM1B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAOlD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAG7D,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sheet/index.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAkB/D,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EACL,aAAa,EACb,WAAW,EACX,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAW1B,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAQhC,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAY9B,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAQjD,OAAO,EACL,uBAAuB,EACvB,yBAAyB,EACzB,gCAAgC,GACjC,MAAM,kBAAkB,CAAC;AAM1B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAOlD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC"}
@@ -1,44 +1,22 @@
1
1
  /**
2
2
  * Scale Bar Types
3
3
  *
4
- * Configurable scale bar for architectural drawings:
5
- * - Multiple visual styles
6
- * - Metric and imperial units
7
- * - Auto-calculation of optimal length
4
+ * Configuration for the scale bar drawn in the sheet's title block
5
+ * (`renderScaleBarInTitleBlock` in `title-block-renderer.ts`, and its
6
+ * on-screen twin in the viewer's `Drawing2DCanvas`). Both draw one
7
+ * alternating-segment metric bar with `0` and end-distance labels; every
8
+ * field below is one that those renderers actually read.
8
9
  */
9
- /** Scale bar style */
10
- export type ScaleBarStyle = 'linear' | 'alternating' | 'single' | 'graphic';
11
- /** Scale bar position relative to drawing viewport */
12
- export type ScaleBarPosition = 'below-viewport' | 'above-viewport' | 'in-title-block' | 'custom';
13
- /** Scale bar unit system */
14
- export type ScaleBarUnits = 'metric' | 'imperial' | 'both';
15
10
  /** Scale bar configuration */
16
11
  export interface ScaleBarConfig {
17
12
  /** Whether to show scale bar */
18
13
  visible: boolean;
19
- /** Scale bar style */
20
- style: ScaleBarStyle;
21
- /** Position */
22
- position: ScaleBarPosition;
23
- /** Custom position offset from default (mm) */
24
- customOffset?: {
25
- x: number;
26
- y: number;
27
- };
28
- /** Unit system */
29
- units: ScaleBarUnits;
30
14
  /** Total length in model units (meters) */
31
15
  totalLengthM: number;
32
16
  /** Number of primary divisions */
33
17
  primaryDivisions: number;
34
- /** Number of subdivisions per primary division */
35
- subdivisions: number;
36
- /** Bar height in mm */
18
+ /** Bar height in mm (clamped to 3mm by the renderer) */
37
19
  heightMm: number;
38
- /** Label font size in mm */
39
- labelFontSize: number;
40
- /** Show unit label (e.g., "meters") */
41
- showUnitLabel: boolean;
42
20
  /** Fill color for filled segments */
43
21
  fillColor: string;
44
22
  /** Stroke color */
@@ -48,6 +26,57 @@ export interface ScaleBarConfig {
48
26
  }
49
27
  /** Default scale bar configuration */
50
28
  export declare const DEFAULT_SCALE_BAR: ScaleBarConfig;
29
+ /**
30
+ * Round distances a scale bar is allowed to span, ascending.
31
+ *
32
+ * Shared by the two things that pick a bar length — the sheet's default
33
+ * ("what should this bar be?") and the title-block renderer's snap ("largest
34
+ * one that fits this cell") — so they cannot disagree about what "nice" means.
35
+ * They were separate literals with identical contents, which is one edit away
36
+ * from divergence.
37
+ *
38
+ * Every entry survives the printed label's formatting exactly: whole
39
+ * centimetres below 1 m, whole metres above. That is the property that makes
40
+ * the snap able to promise an exact label, so a new entry has to keep it —
41
+ * 0.15 would print "15cm" correctly but 0.125 would print "13cm" and lie.
42
+ *
43
+ * Reaches 0.02 because `maxBarWidth` is `min(width * 0.3, 50)`. On the shipped
44
+ * compact preset (120 mm) that is 36 mm, and stopping the ladder at 0.1 left
45
+ * both 1:1 and 1:2 with no bar at all. At the full 50 mm only 1:1 fails, since
46
+ * 0.1 m at 1:2 is exactly 50 mm — so 36 is the binding number, which is what
47
+ * `scale-bar-ladder.test.ts` asserts against.
48
+ */
49
+ export declare const NICE_BAR_LENGTHS_M: readonly number[];
50
+ /**
51
+ * Whether `m` survives the scale bar's label formatting without lying:
52
+ * whole centimetres below 1 m, whole metres above.
53
+ */
54
+ export declare function barLabelIsExact(m: number): boolean;
55
+ /**
56
+ * Smallest SEGMENT worth drawing, in millimetres of paper.
57
+ *
58
+ * Segment widths are emitted through `toFixed(2)`, so anything under 0.005 mm
59
+ * prints `width="0.00"` — a label with no bar under it. That is reachable: at
60
+ * 1:1 on a 760 m model the transform gives 0.42 mm per metre, so a 5 cm bar is
61
+ * 0.02 mm wide and all five of its segments print zero. The old clamp-to-cell
62
+ * hid this by always stretching the bar to fill the cell; choosing the distance
63
+ * first removed that accident, so the floor has to be explicit.
64
+ *
65
+ * Deliberately a per-SEGMENT bound rather than a whole-bar one. A blanket bar
66
+ * minimum of 5 mm rejects a perfectly legible 4 mm bar at 1:500, which is a
67
+ * real configuration; what actually fails is a segment rounding to zero, and
68
+ * that is what this measures. 0.01 mm is twice the rounding threshold.
69
+ *
70
+ * A correction to an earlier version of this note, because it misattributed
71
+ * the mechanism: the old `Math.min(barLengthMm, maxBarWidth)` clamp could not
72
+ * ENLARGE anything. What filled the cell was the reciprocal-units bug — at
73
+ * 0.42 mm per metre the old formula computed `0.05 * 1000 / 0.42` = 119 mm and
74
+ * the clamp capped that at 50. So one bug was hiding another, which is why
75
+ * fixing the arithmetic is what exposed this.
76
+ */
77
+ export declare const MIN_BAR_SEGMENT_MM = 0.01;
78
+ /** The end label the scale bar prints for a given distance. */
79
+ export declare function formatBarLabel(m: number): string;
51
80
  /**
52
81
  * Calculate optimal scale bar length based on drawing scale
53
82
  * Returns length in model units (meters)
@@ -63,18 +92,19 @@ export declare function calculateOptimalScaleBarLength(scaleFactor: number, maxL
63
92
  export declare function calculateOptimalDivisions(totalLengthM: number): number;
64
93
  /** North arrow style */
65
94
  export type NorthArrowStyle = 'simple' | 'compass' | 'decorative' | 'none';
66
- /** North arrow configuration */
95
+ /**
96
+ * North arrow configuration.
97
+ *
98
+ * The arrow is drawn at a fixed spot in the title block, so there is no
99
+ * position field: `style` only selects between drawn ('simple', 'compass',
100
+ * 'decorative' all render the same glyph today) and not drawn ('none').
101
+ */
67
102
  export interface NorthArrowConfig {
68
- /** Arrow style */
103
+ /** Arrow style; 'none' suppresses the arrow */
69
104
  style: NorthArrowStyle;
70
105
  /** Rotation in degrees (0 = up) */
71
106
  rotation: number;
72
- /** Position in mm from top-left of viewport */
73
- positionMm: {
74
- x: number;
75
- y: number;
76
- };
77
- /** Size in mm */
107
+ /** Size in mm (clamped by the renderer to 8mm and to the title block) */
78
108
  sizeMm: number;
79
109
  }
80
110
  /** Default north arrow configuration */
@@ -1 +1 @@
1
- {"version":3,"file":"scale-bar-types.d.ts","sourceRoot":"","sources":["../../src/sheet/scale-bar-types.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AAEH,sBAAsB;AACtB,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,aAAa,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE5E,sDAAsD;AACtD,MAAM,MAAM,gBAAgB,GAAG,gBAAgB,GAAG,gBAAgB,GAAG,gBAAgB,GAAG,QAAQ,CAAC;AAEjG,4BAA4B;AAC5B,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,CAAC;AAE3D,8BAA8B;AAC9B,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,sBAAsB;IACtB,KAAK,EAAE,aAAa,CAAC;IACrB,eAAe;IACf,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,+CAA+C;IAC/C,YAAY,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,kBAAkB;IAClB,KAAK,EAAE,aAAa,CAAC;IACrB,2CAA2C;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,kCAAkC;IAClC,gBAAgB,EAAE,MAAM,CAAC;IACzB,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,4BAA4B;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,aAAa,EAAE,OAAO,CAAC;IACvB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,mBAAmB;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,sCAAsC;AACtC,eAAO,MAAM,iBAAiB,EAAE,cAc/B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAC5C,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,MAAM,CAoBR;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAMtE;AAED,wBAAwB;AACxB,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,MAAM,CAAC;AAE3E,gCAAgC;AAChC,MAAM,WAAW,gBAAgB;IAC/B,kBAAkB;IAClB,KAAK,EAAE,eAAe,CAAC;IACvB,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,UAAU,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC,iBAAiB;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wCAAwC;AACxC,eAAO,MAAM,mBAAmB,EAAE,gBAKjC,CAAC"}
1
+ {"version":3,"file":"scale-bar-types.d.ts","sourceRoot":"","sources":["../../src/sheet/scale-bar-types.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,8BAA8B;AAC9B,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,2CAA2C;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,kCAAkC;IAClC,gBAAgB,EAAE,MAAM,CAAC;IACzB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,mBAAmB;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,sCAAsC;AACtC,eAAO,MAAM,iBAAiB,EAAE,cAQ/B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAE/C,CAAC;AAEF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAsBlD;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAEvC,+DAA+D;AAC/D,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAC5C,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,MAAM,CAeR;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAMtE;AAED,wBAAwB;AACxB,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,MAAM,CAAC;AAE3E;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,KAAK,EAAE,eAAe,CAAC;IACvB,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wCAAwC;AACxC,eAAO,MAAM,mBAAmB,EAAE,gBAIjC,CAAC"}
@@ -4,19 +4,91 @@
4
4
  /** Default scale bar configuration */
5
5
  export const DEFAULT_SCALE_BAR = {
6
6
  visible: true,
7
- style: 'alternating',
8
- position: 'below-viewport',
9
- units: 'metric',
10
7
  totalLengthM: 5, // Will be auto-calculated based on scale
11
8
  primaryDivisions: 5,
12
- subdivisions: 2,
13
9
  heightMm: 3,
14
- labelFontSize: 2.5,
15
- showUnitLabel: true,
16
10
  fillColor: '#000000',
17
11
  strokeColor: '#000000',
18
12
  lineWeight: 0.25,
19
13
  };
14
+ /**
15
+ * Round distances a scale bar is allowed to span, ascending.
16
+ *
17
+ * Shared by the two things that pick a bar length — the sheet's default
18
+ * ("what should this bar be?") and the title-block renderer's snap ("largest
19
+ * one that fits this cell") — so they cannot disagree about what "nice" means.
20
+ * They were separate literals with identical contents, which is one edit away
21
+ * from divergence.
22
+ *
23
+ * Every entry survives the printed label's formatting exactly: whole
24
+ * centimetres below 1 m, whole metres above. That is the property that makes
25
+ * the snap able to promise an exact label, so a new entry has to keep it —
26
+ * 0.15 would print "15cm" correctly but 0.125 would print "13cm" and lie.
27
+ *
28
+ * Reaches 0.02 because `maxBarWidth` is `min(width * 0.3, 50)`. On the shipped
29
+ * compact preset (120 mm) that is 36 mm, and stopping the ladder at 0.1 left
30
+ * both 1:1 and 1:2 with no bar at all. At the full 50 mm only 1:1 fails, since
31
+ * 0.1 m at 1:2 is exactly 50 mm — so 36 is the binding number, which is what
32
+ * `scale-bar-ladder.test.ts` asserts against.
33
+ */
34
+ export const NICE_BAR_LENGTHS_M = [
35
+ 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000,
36
+ ];
37
+ /**
38
+ * Whether `m` survives the scale bar's label formatting without lying:
39
+ * whole centimetres below 1 m, whole metres above.
40
+ */
41
+ export function barLabelIsExact(m) {
42
+ // States the contract explicitly. The arithmetic below happens to reject all
43
+ // of these too — -5 fails the `>= 1` floor, NaN and Infinity fail because
44
+ // `Math.abs(NaN - NaN)` is not `< 1e-9` — so removing this line changes no
45
+ // test. It is kept as documentation a reader does not have to derive, not as
46
+ // a load-bearing check.
47
+ if (!(m > 0) || !Number.isFinite(m))
48
+ return false;
49
+ // Symmetric epsilon on both branches. `Number.isInteger` alone is too strict
50
+ // for the float artefacts the ladder itself carries (`0.02 * 100` is
51
+ // 2.0000000000000004), and `Math.round(m * 1000) / 10` was too loose in the
52
+ // other direction: it tolerates ±0.5 mm, so 0.0201 m passed and printed
53
+ // "2cm" over a bar 0.1 mm longer than that.
54
+ //
55
+ // The `>= 1` is what stops a sub-centimetre length passing. Without it,
56
+ // anything under 0.005 m rounds to zero centimetres, which is an integer, so
57
+ // it was "exact" — and drew a 0.4 mm bar labelled "0cm". A bar claiming to
58
+ // span no distance is less honest than no bar.
59
+ if (m < 1) {
60
+ const cm = m * 100;
61
+ return Math.abs(cm - Math.round(cm)) < 1e-9 && Math.round(cm) >= 1;
62
+ }
63
+ return Math.abs(m - Math.round(m)) < 1e-9;
64
+ }
65
+ /**
66
+ * Smallest SEGMENT worth drawing, in millimetres of paper.
67
+ *
68
+ * Segment widths are emitted through `toFixed(2)`, so anything under 0.005 mm
69
+ * prints `width="0.00"` — a label with no bar under it. That is reachable: at
70
+ * 1:1 on a 760 m model the transform gives 0.42 mm per metre, so a 5 cm bar is
71
+ * 0.02 mm wide and all five of its segments print zero. The old clamp-to-cell
72
+ * hid this by always stretching the bar to fill the cell; choosing the distance
73
+ * first removed that accident, so the floor has to be explicit.
74
+ *
75
+ * Deliberately a per-SEGMENT bound rather than a whole-bar one. A blanket bar
76
+ * minimum of 5 mm rejects a perfectly legible 4 mm bar at 1:500, which is a
77
+ * real configuration; what actually fails is a segment rounding to zero, and
78
+ * that is what this measures. 0.01 mm is twice the rounding threshold.
79
+ *
80
+ * A correction to an earlier version of this note, because it misattributed
81
+ * the mechanism: the old `Math.min(barLengthMm, maxBarWidth)` clamp could not
82
+ * ENLARGE anything. What filled the cell was the reciprocal-units bug — at
83
+ * 0.42 mm per metre the old formula computed `0.05 * 1000 / 0.42` = 119 mm and
84
+ * the clamp capped that at 50. So one bug was hiding another, which is why
85
+ * fixing the arithmetic is what exposed this.
86
+ */
87
+ export const MIN_BAR_SEGMENT_MM = 0.01;
88
+ /** The end label the scale bar prints for a given distance. */
89
+ export function formatBarLabel(m) {
90
+ return m < 1 ? `${(m * 100).toFixed(0)}cm` : `${m.toFixed(0)}m`;
91
+ }
20
92
  /**
21
93
  * Calculate optimal scale bar length based on drawing scale
22
94
  * Returns length in model units (meters)
@@ -29,11 +101,7 @@ export function calculateOptimalScaleBarLength(scaleFactor, maxLengthMm) {
29
101
  const targetPaperLengthMm = Math.min(80, maxLengthMm * 0.8);
30
102
  // Convert paper length to model units (meters)
31
103
  const modelLength = (targetPaperLengthMm * scaleFactor) / 1000;
32
- // Round to nice numbers for readability
33
- const niceNumbers = [
34
- 0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000,
35
- ];
36
- for (const n of niceNumbers) {
104
+ for (const n of NICE_BAR_LENGTHS_M) {
37
105
  if (n >= modelLength * 0.5 && n <= modelLength * 1.5) {
38
106
  return n;
39
107
  }
@@ -60,7 +128,6 @@ export function calculateOptimalDivisions(totalLengthM) {
60
128
  export const DEFAULT_NORTH_ARROW = {
61
129
  style: 'simple',
62
130
  rotation: 0,
63
- positionMm: { x: 30, y: 30 },
64
131
  sizeMm: 15,
65
132
  };
66
133
  //# sourceMappingURL=scale-bar-types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"scale-bar-types.js","sourceRoot":"","sources":["../../src/sheet/scale-bar-types.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAoD/D,sCAAsC;AACtC,MAAM,CAAC,MAAM,iBAAiB,GAAmB;IAC/C,OAAO,EAAE,IAAI;IACb,KAAK,EAAE,aAAa;IACpB,QAAQ,EAAE,gBAAgB;IAC1B,KAAK,EAAE,QAAQ;IACf,YAAY,EAAE,CAAC,EAAE,yCAAyC;IAC1D,gBAAgB,EAAE,CAAC;IACnB,YAAY,EAAE,CAAC;IACf,QAAQ,EAAE,CAAC;IACX,aAAa,EAAE,GAAG;IAClB,aAAa,EAAE,IAAI;IACnB,SAAS,EAAE,SAAS;IACpB,WAAW,EAAE,SAAS;IACtB,UAAU,EAAE,IAAI;CACjB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,8BAA8B,CAC5C,WAAmB,EACnB,WAAmB;IAEnB,2CAA2C;IAC3C,MAAM,mBAAmB,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,WAAW,GAAG,GAAG,CAAC,CAAC;IAE5D,+CAA+C;IAC/C,MAAM,WAAW,GAAG,CAAC,mBAAmB,GAAG,WAAW,CAAC,GAAG,IAAI,CAAC;IAE/D,wCAAwC;IACxC,MAAM,WAAW,GAAG;QAClB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI;KACxD,CAAC;IAEF,KAAK,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,WAAW,GAAG,GAAG,IAAI,CAAC,IAAI,WAAW,GAAG,GAAG,EAAE,CAAC;YACrD,OAAO,CAAC,CAAC;QACX,CAAC;IACH,CAAC;IAED,6BAA6B;IAC7B,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;AACjC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,YAAoB;IAC5D,IAAI,YAAY,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IAChC,IAAI,YAAY,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IAChC,IAAI,YAAY,IAAI,EAAE;QAAE,OAAO,CAAC,CAAC;IACjC,IAAI,YAAY,IAAI,EAAE;QAAE,OAAO,CAAC,CAAC;IACjC,OAAO,CAAC,CAAC,CAAC,yBAAyB;AACrC,CAAC;AAiBD,wCAAwC;AACxC,MAAM,CAAC,MAAM,mBAAmB,GAAqB;IACnD,KAAK,EAAE,QAAQ;IACf,QAAQ,EAAE,CAAC;IACX,UAAU,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE;IAC5B,MAAM,EAAE,EAAE;CACX,CAAC"}
1
+ {"version":3,"file":"scale-bar-types.js","sourceRoot":"","sources":["../../src/sheet/scale-bar-types.ts"],"names":[],"mappings":"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"}
@@ -81,4 +81,57 @@ export declare function calculateDrawingTransform(drawingBounds: {
81
81
  translateY: number;
82
82
  scaleFactor: number;
83
83
  };
84
+ /**
85
+ * `calculateDrawingTransform` always derives `translateY` assuming the
86
+ * caller will map model Y to paper Y with a sign flip (`adjustedY = -y`,
87
+ * matching a plan view drawn with north/Y+ up on paper) — it centers using
88
+ * `drawingBounds.maxY` for exactly that reason (see the "Flip Y" comment
89
+ * above).
90
+ *
91
+ * Plan ('down') sections do NOT flip Y (see `Drawing2DCanvas.tsx`'s
92
+ * `flipY = sectionAxis !== 'down'`): they map `adjustedY = y` directly, so
93
+ * north stays up without a sign flip. Reusing the flipped `translateY`
94
+ * unmodified in that case still centers the WIDTH but shifts the centered Y
95
+ * off by `(minY + maxY) * scaleFactor` — the drawing lands off-center on the
96
+ * sheet, worse the further the section's bounds sit from being symmetric
97
+ * about Y=0. This offset was found diverging between the sheet PREVIEW
98
+ * (`Drawing2DCanvas.tsx`, which already carried this correction) and the
99
+ * sheet PRINT/EXPORT path (`useDrawingExport.ts`'s `generateSheetSVG`, which
100
+ * did not) — issue #2940.
101
+ *
102
+ * Call this instead of `calculateDrawingTransform` directly wherever the
103
+ * axis's flip behavior is known, so every sheet consumer (screen preview,
104
+ * print, SVG/PDF export) derives the flip correction from one place rather
105
+ * than each re-deriving it. Note this function is the geometry only: it
106
+ * takes no view state, so two callers still agree only insofar as they pass
107
+ * the same `flipY`/`flipX` AND resolve any cached/pinned placement the same
108
+ * way. The viewer routes both sheet consumers through one resolver
109
+ * (`apps/viewer/src/lib/drawing/sheet-transform.ts`) for exactly that
110
+ * reason.
111
+ *
112
+ * `translateX` needs the mirror-image correction, but gated the OPPOSITE
113
+ * way from `translateY`: `calculateDrawingTransform`'s `translateX` bakes
114
+ * in the assumption that the caller maps model X to paper X with NO sign
115
+ * flip (`adjustedX = x`) — unlike `translateY`, which bakes in a flip. So
116
+ * `translateX` is only wrong when `flipX` is actually true (`adjustedX =
117
+ * -x`, used for 'side' sections to view from the conventional direction,
118
+ * see `Drawing2DCanvas.tsx` / `useDrawingExport.ts`'s `flipX = sectionAxis
119
+ * === 'side'`): flipping negates and reverses the drawing's X extent to
120
+ * `[-maxX, -minX]`, so the base translate (keyed off `-minX`) shifts the
121
+ * centered box by `(minX + maxX) * scaleFactor` — the drawing lands off
122
+ * the left/right edge of the sheet, worse the further the section's bounds
123
+ * sit from being symmetric about X=0. Confirmed unfixed prior to this
124
+ * change (issue #2940's X-axis half): a 'side' section with bounds minX=2,
125
+ * maxX=12 on a 190mm-wide viewport landed 140mm off-center.
126
+ */
127
+ export declare function calculateDrawingTransformForAxis(drawingBounds: {
128
+ minX: number;
129
+ minY: number;
130
+ maxX: number;
131
+ maxY: number;
132
+ }, viewportBounds: ViewportBounds, scale: DrawingScale, flipY: boolean, flipX?: boolean): {
133
+ translateX: number;
134
+ translateY: number;
135
+ scaleFactor: number;
136
+ };
84
137
  //# sourceMappingURL=sheet-types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sheet-types.d.ts","sourceRoot":"","sources":["../../src/sheet/sheet-types.ts"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC7B,8CAA8C;IAC9C,CAAC,EAAE,MAAM,CAAC;IACV,6CAA6C;IAC7C,CAAC,EAAE,MAAM,CAAC;IACV,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B,8BAA8B;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,KAAK,EAAE,mBAAmB,CAAC;IAC3B,kCAAkC;IAClC,KAAK,EAAE,YAAY,CAAC;IACpB,gCAAgC;IAChC,UAAU,EAAE,gBAAgB,CAAC;IAC7B,8BAA8B;IAC9B,QAAQ,EAAE,cAAc,CAAC;IACzB,oBAAoB;IACpB,KAAK,EAAE,YAAY,CAAC;IACpB,gCAAgC;IAChC,UAAU,EAAE,gBAAgB,CAAC;IAC7B,8DAA8D;IAC9D,cAAc,EAAE,cAAc,CAAC;IAC/B,uBAAuB;IACvB,SAAS,EAAE,aAAa,EAAE,CAAC;CAC5B;AAED,6BAA6B;AAC7B,MAAM,WAAW,oBAAoB;IACnC,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yBAAyB;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oBAAoB;IACpB,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,mBAAmB,EAC1B,KAAK,EAAE,YAAY,EACnB,UAAU,EAAE,gBAAgB,GAC3B,cAAc,CA4ChB;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACvC,aAAa,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACzE,cAAc,EAAE,cAAc,EAC9B,KAAK,EAAE,YAAY,GAClB;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB,CAgCA"}
1
+ {"version":3,"file":"sheet-types.d.ts","sourceRoot":"","sources":["../../src/sheet/sheet-types.ts"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC7B,8CAA8C;IAC9C,CAAC,EAAE,MAAM,CAAC;IACV,6CAA6C;IAC7C,CAAC,EAAE,MAAM,CAAC;IACV,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B,8BAA8B;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,KAAK,EAAE,mBAAmB,CAAC;IAC3B,kCAAkC;IAClC,KAAK,EAAE,YAAY,CAAC;IACpB,gCAAgC;IAChC,UAAU,EAAE,gBAAgB,CAAC;IAC7B,8BAA8B;IAC9B,QAAQ,EAAE,cAAc,CAAC;IACzB,oBAAoB;IACpB,KAAK,EAAE,YAAY,CAAC;IACpB,gCAAgC;IAChC,UAAU,EAAE,gBAAgB,CAAC;IAC7B,8DAA8D;IAC9D,cAAc,EAAE,cAAc,CAAC;IAC/B,uBAAuB;IACvB,SAAS,EAAE,aAAa,EAAE,CAAC;CAC5B;AAED,6BAA6B;AAC7B,MAAM,WAAW,oBAAoB;IACnC,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yBAAyB;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oBAAoB;IACpB,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,mBAAmB,EAC1B,KAAK,EAAE,YAAY,EACnB,UAAU,EAAE,gBAAgB,GAC3B,cAAc,CA4ChB;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACvC,aAAa,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACzE,cAAc,EAAE,cAAc,EAC9B,KAAK,EAAE,YAAY,GAClB;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB,CAgCA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,gCAAgC,CAC9C,aAAa,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACzE,cAAc,EAAE,cAAc,EAC9B,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,OAAO,EACd,KAAK,UAAQ,GACZ;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB,CASA"}
@@ -74,4 +74,57 @@ export function calculateDrawingTransform(drawingBounds, viewportBounds, scale)
74
74
  drawingBounds.maxY * scaleFactor; // Flip Y
75
75
  return { translateX, translateY, scaleFactor };
76
76
  }
77
+ /**
78
+ * `calculateDrawingTransform` always derives `translateY` assuming the
79
+ * caller will map model Y to paper Y with a sign flip (`adjustedY = -y`,
80
+ * matching a plan view drawn with north/Y+ up on paper) — it centers using
81
+ * `drawingBounds.maxY` for exactly that reason (see the "Flip Y" comment
82
+ * above).
83
+ *
84
+ * Plan ('down') sections do NOT flip Y (see `Drawing2DCanvas.tsx`'s
85
+ * `flipY = sectionAxis !== 'down'`): they map `adjustedY = y` directly, so
86
+ * north stays up without a sign flip. Reusing the flipped `translateY`
87
+ * unmodified in that case still centers the WIDTH but shifts the centered Y
88
+ * off by `(minY + maxY) * scaleFactor` — the drawing lands off-center on the
89
+ * sheet, worse the further the section's bounds sit from being symmetric
90
+ * about Y=0. This offset was found diverging between the sheet PREVIEW
91
+ * (`Drawing2DCanvas.tsx`, which already carried this correction) and the
92
+ * sheet PRINT/EXPORT path (`useDrawingExport.ts`'s `generateSheetSVG`, which
93
+ * did not) — issue #2940.
94
+ *
95
+ * Call this instead of `calculateDrawingTransform` directly wherever the
96
+ * axis's flip behavior is known, so every sheet consumer (screen preview,
97
+ * print, SVG/PDF export) derives the flip correction from one place rather
98
+ * than each re-deriving it. Note this function is the geometry only: it
99
+ * takes no view state, so two callers still agree only insofar as they pass
100
+ * the same `flipY`/`flipX` AND resolve any cached/pinned placement the same
101
+ * way. The viewer routes both sheet consumers through one resolver
102
+ * (`apps/viewer/src/lib/drawing/sheet-transform.ts`) for exactly that
103
+ * reason.
104
+ *
105
+ * `translateX` needs the mirror-image correction, but gated the OPPOSITE
106
+ * way from `translateY`: `calculateDrawingTransform`'s `translateX` bakes
107
+ * in the assumption that the caller maps model X to paper X with NO sign
108
+ * flip (`adjustedX = x`) — unlike `translateY`, which bakes in a flip. So
109
+ * `translateX` is only wrong when `flipX` is actually true (`adjustedX =
110
+ * -x`, used for 'side' sections to view from the conventional direction,
111
+ * see `Drawing2DCanvas.tsx` / `useDrawingExport.ts`'s `flipX = sectionAxis
112
+ * === 'side'`): flipping negates and reverses the drawing's X extent to
113
+ * `[-maxX, -minX]`, so the base translate (keyed off `-minX`) shifts the
114
+ * centered box by `(minX + maxX) * scaleFactor` — the drawing lands off
115
+ * the left/right edge of the sheet, worse the further the section's bounds
116
+ * sit from being symmetric about X=0. Confirmed unfixed prior to this
117
+ * change (issue #2940's X-axis half): a 'side' section with bounds minX=2,
118
+ * maxX=12 on a 190mm-wide viewport landed 140mm off-center.
119
+ */
120
+ export function calculateDrawingTransformForAxis(drawingBounds, viewportBounds, scale, flipY, flipX = false) {
121
+ const base = calculateDrawingTransform(drawingBounds, viewportBounds, scale);
122
+ const translateY = flipY
123
+ ? base.translateY
124
+ : base.translateY - (drawingBounds.maxY + drawingBounds.minY) * base.scaleFactor;
125
+ const translateX = flipX
126
+ ? base.translateX + (drawingBounds.minX + drawingBounds.maxX) * base.scaleFactor
127
+ : base.translateX;
128
+ return { ...base, translateX, translateY };
129
+ }
77
130
  //# sourceMappingURL=sheet-types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sheet-types.js","sourceRoot":"","sources":["../../src/sheet/sheet-types.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAmE/D;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CACrC,KAA0B,EAC1B,KAAmB,EACnB,UAA4B;IAE5B,oBAAoB;IACpB,MAAM,cAAc,GAClB,KAAK,CAAC,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,aAAa,GAAG,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC;IAC5E,MAAM,eAAe,GACnB,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC;IAC/D,MAAM,aAAa,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC;IACjE,MAAM,gBAAgB,GACpB,KAAK,CAAC,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC;IAEjE,IAAI,SAAS,GAAG,cAAc,CAAC;IAC/B,IAAI,SAAS,GAAG,aAAa,CAAC;IAC9B,IAAI,aAAa,GAAG,eAAe,GAAG,cAAc,CAAC;IACrD,IAAI,cAAc,GAAG,gBAAgB,GAAG,aAAa,CAAC;IAEtD,kCAAkC;IAClC,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,uCAAuC;IAE1D,QAAQ,UAAU,CAAC,QAAQ,EAAE,CAAC;QAC5B,KAAK,cAAc;YACjB,wCAAwC;YACxC,sEAAsE;YACtE,cAAc,GAAG,gBAAgB,GAAG,aAAa,GAAG,UAAU,CAAC,QAAQ,GAAG,OAAO,CAAC;YAClF,MAAM;QAER,KAAK,aAAa;YAChB,yCAAyC;YACzC,cAAc;gBACZ,gBAAgB,GAAG,aAAa,GAAG,UAAU,CAAC,QAAQ,GAAG,OAAO,CAAC;YACnE,MAAM;QAER,KAAK,aAAa;YAChB,2CAA2C;YAC3C,aAAa;gBACX,eAAe,GAAG,cAAc,GAAG,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC;YAClE,MAAM;IACV,CAAC;IAED,OAAO;QACL,CAAC,EAAE,SAAS;QACZ,CAAC,EAAE,SAAS;QACZ,KAAK,EAAE,aAAa;QACpB,MAAM,EAAE,cAAc;KACvB,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CACvC,aAAyE,EACzE,cAA8B,EAC9B,KAAmB;IAMnB,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC;IAC7D,MAAM,aAAa,GAAG,aAAa,CAAC,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC;IAE9D,kDAAkD;IAClD,oCAAoC;IACpC,MAAM,UAAU,GAAG,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC;IACvC,MAAM,cAAc,GAAG,YAAY,GAAG,UAAU,CAAC;IACjD,MAAM,eAAe,GAAG,aAAa,GAAG,UAAU,CAAC;IAEnD,yDAAyD;IACzD,MAAM,aAAa,GAAG,IAAI,CAAC;IAC3B,MAAM,MAAM,GAAG,CAAC,cAAc,CAAC,KAAK,GAAG,aAAa,CAAC,GAAG,cAAc,CAAC;IACvE,MAAM,MAAM,GAAG,CAAC,cAAc,CAAC,MAAM,GAAG,aAAa,CAAC,GAAG,eAAe,CAAC;IACzE,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,4BAA4B;IAE1E,MAAM,WAAW,GAAG,UAAU,GAAG,QAAQ,CAAC;IAE1C,iCAAiC;IACjC,MAAM,YAAY,GAAG,YAAY,GAAG,WAAW,CAAC;IAChD,MAAM,aAAa,GAAG,aAAa,GAAG,WAAW,CAAC;IAElD,MAAM,UAAU,GACd,cAAc,CAAC,CAAC;QAChB,CAAC,cAAc,CAAC,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC;QACzC,aAAa,CAAC,IAAI,GAAG,WAAW,CAAC;IACnC,MAAM,UAAU,GACd,cAAc,CAAC,CAAC;QAChB,CAAC,cAAc,CAAC,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC;QAC3C,aAAa,CAAC,IAAI,GAAG,WAAW,CAAC,CAAC,SAAS;IAE7C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC;AACjD,CAAC"}
1
+ {"version":3,"file":"sheet-types.js","sourceRoot":"","sources":["../../src/sheet/sheet-types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"title-block-renderer.d.ts","sourceRoot":"","sources":["../../src/sheet/title-block-renderer.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAsB,MAAM,wBAAwB,CAAC;AAClG,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,oGAAoG;IACpG,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB;IACrC,uCAAuC;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACjE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,gBAAgB,EACxB,gBAAgB,EAAE,gBAAgB,EAClC,SAAS,GAAE,aAAa,EAAO,EAC/B,MAAM,CAAC,EAAE,gBAAgB,GACxB,sBAAsB,CAgFxB"}
1
+ {"version":3,"file":"title-block-renderer.d.ts","sourceRoot":"","sources":["../../src/sheet/title-block-renderer.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAsB,MAAM,wBAAwB,CAAC;AAElG,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,oGAAoG;IACpG,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB;IACrC,uCAAuC;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACjE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,gBAAgB,EACxB,gBAAgB,EAAE,gBAAgB,EAClC,SAAS,GAAE,aAAa,EAAO,EAC/B,MAAM,CAAC,EAAE,gBAAgB,GACxB,sBAAsB,CAgFxB"}
@@ -1,6 +1,7 @@
1
1
  /* This Source Code Form is subject to the terms of the Mozilla Public
2
2
  * License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ import { NICE_BAR_LENGTHS_M, barLabelIsExact, formatBarLabel, MIN_BAR_SEGMENT_MM } from './scale-bar-types.js';
4
5
  /**
5
6
  * Render a title block to SVG
6
7
  */
@@ -257,18 +258,116 @@ function renderRevisionHistory(revisions, x, y, w, h, config) {
257
258
  */
258
259
  function renderScaleBarInTitleBlock(scaleBar, scale, x, y, w, h, _hasLogo, effectiveScaleFactor) {
259
260
  let svg = ' <g id="title-block-scale-bar">\n';
260
- // Use effective scale factor if provided (for dynamic scaling), otherwise use configured scale
261
- const scaleFactor = effectiveScaleFactor ?? scale.factor;
261
+ // These two arguments are in RECIPROCAL units, and this function used to
262
+ // substitute one for the other in a single formula:
263
+ //
264
+ // effectiveScaleFactor millimetres per metre (10 at 1:100)
265
+ // scale.factor the N of 1:N (100 at 1:100)
266
+ //
267
+ // `calculateDrawingTransform` returns the former — `sheet-types.ts` uses it
268
+ // as `drawingWidth * scaleFactor` to get millimetres — its own doc says "mm
269
+ // per meter", and `useDrawingExport` passes it on EVERY export. So the branch
270
+ // the exporter always takes was the wrong one, and every exported bar was
271
+ // labelled wrong at every scale: a 5 m bar read 0.5 m at 1:100, and 0.1 m at
272
+ // 1:500 where 25 m is right. Normalise to one unit so there is one formula.
273
+ const mmPerMetre = effectiveScaleFactor ?? 1000 / scale.factor;
262
274
  // Position: bottom left with small margin
263
275
  const barX = x + 3;
264
276
  const barY = y + h - 8; // 8mm from bottom (leaves room for label)
265
277
  const maxBarWidth = Math.min(w * 0.3, 50);
266
- const barLengthMm = (scaleBar.totalLengthM * 1000) / scaleFactor;
267
- const actualBarLength = Math.min(barLengthMm, maxBarWidth);
268
- const barHeight = Math.min(scaleBar.heightMm, 3);
269
- const actualTotalLength = (actualBarLength * scaleFactor) / 1000;
270
- // Draw alternating bar segments
271
- const divisions = scaleBar.primaryDivisions;
278
+ const barLengthMm = scaleBar.totalLengthM * mmPerMetre;
279
+ // Checked BEFORE the clamp, and the ordering is load-bearing: testing the
280
+ // clamped result loses the infinite case, because `Math.min(Infinity, 50)`
281
+ // is a perfectly finite 50.
282
+ //
283
+ // `mmPerMetre > 0` is checked SEPARATELY and is not redundant with
284
+ // `barLengthMm > 0`. An earlier version claimed one check on the product
285
+ // covered "any combination" of the two operands. It does not: two negatives
286
+ // cancel, so `totalLengthM: -5` with `effectiveScaleFactor: -10` gives
287
+ // `barLengthMm` 50 and passes every clause, after which the labels print
288
+ // NEGATIVE distances on a bar that draws normally. That is worse than the
289
+ // malformed rect this guard was written to stop, because nothing about the
290
+ // output looks wrong. Credit to CodeRabbit for the case and to
291
+ // @bimvoice-01 for relaying it.
292
+ //
293
+ // Deliberately not recording which of the sign checks in this file is
294
+ // load-bearing. Three versions of this paragraph did — "the snap masks
295
+ // this", then "this is load-bearing", then "three things reject it" — and
296
+ // each was a correct measurement made false by the next commit, twice by my
297
+ // own hand within one session and once by a revert. A mutation result is a
298
+ // fact about the whole program, so anything anywhere invalidates it, while
299
+ // it reads like a durable property. The commit message is where such a
300
+ // measurement belongs, pinned to a tree.
301
+ //
302
+ // Without this the bar emitted `<rect width="-10.00">`, invalid SVG dressed
303
+ // as a drawing. Drawing nothing is the honest degradation: a missing scale
304
+ // bar is visibly missing.
305
+ if (!(mmPerMetre > 0) ||
306
+ !(barLengthMm > 0) ||
307
+ !Number.isFinite(barLengthMm) ||
308
+ !(maxBarWidth > 0)) {
309
+ return '';
310
+ }
311
+ // Snap to a ROUND distance that fits, rather than clamping to the cell width
312
+ // and rounding the label afterwards. Folding the clamp into the label is not
313
+ // enough on its own: the label prints to 0 dp, so a 120 mm title block (a
314
+ // shipped preset) caps a 5 m bar at 36 mm = 3.6 m and prints "4m" — 11% off
315
+ // on a printed sheet, and up to ~33% for a bar landing near 1.5 m.
316
+ //
317
+ // Choosing the distance first makes the label exact by construction.
318
+ //
319
+ // It does NOT yet make the export agree with the on-screen preview, and an
320
+ // earlier version of this comment claimed it did. Measured over nine common
321
+ // (block width x scale) pairs with the default 5 m bar, six disagree:
322
+ // `Drawing2DCanvas` halves/doubles from the configured length, clamps to the
323
+ // cell, and rounds the label from the clamped value — the same clamp-then-
324
+ // round shape this function just stopped doing, so at a 120 mm block and
325
+ // 1:100 the preview still prints "3m" over a 2.5 m bar while the export
326
+ // prints an exact "2m". Unifying them means moving the preview onto
327
+ // `NICE_BAR_LENGTHS_M` too, which is a viewer change and not this diff.
328
+ // `divisions`, bounded at BOTH ends. The low end stops 0 dividing and 2.5 drawing three
329
+ // segments of width L/2.5 (the loop runs while `i < 2.5`), overflowing the
330
+ // bar by 20%. The high end matters too: 100000 divisions emits an 11.5 MB
331
+ // group of 0.0005 mm rects, and 1e7 is about a gigabyte — it hangs the export
332
+ // rather than drawing a bad bar. Nothing legible needs more than 20.
333
+ const divisions = Number.isFinite(scaleBar.primaryDivisions)
334
+ ? Math.min(20, Math.max(1, Math.floor(scaleBar.primaryDivisions)))
335
+ : 1;
336
+ const requestedM = scaleBar.totalLengthM;
337
+ // Fits the cell AND draws segments that survive `toFixed(2)`. Without the
338
+ // lower bound a bar can be chosen whose segments all print `width="0.00"`
339
+ // under a confident label — a label with no bar, which is the dishonesty
340
+ // this function refuses everywhere else. Reachable at 1:1 on a large model,
341
+ // where the transform gives well under 1 mm per metre.
342
+ const fits = (m) => m * mmPerMetre <= maxBarWidth + 1e-9 &&
343
+ (m * mmPerMetre) / divisions >= MIN_BAR_SEGMENT_MM - 1e-12;
344
+ // Keep the requested length when it already fits AND already labels exactly.
345
+ // Snapping unconditionally shrank bars that were fine: a 4 m bar at 1:100 is
346
+ // 40 mm in a 50 mm cell and prints "4m" exactly, and snapping it to the
347
+ // nearest ladder value below halved it to 2 m for no reason.
348
+ let actualTotalLength = fits(requestedM) && barLabelIsExact(requestedM)
349
+ ? requestedM
350
+ : undefined;
351
+ if (actualTotalLength === undefined) {
352
+ // Otherwise take the largest round distance that fits and does not claim
353
+ // more than was asked for.
354
+ const candidates = NICE_BAR_LENGTHS_M.filter((m) => m <= requestedM + 1e-9 && fits(m));
355
+ actualTotalLength = candidates[candidates.length - 1];
356
+ }
357
+ if (actualTotalLength === undefined) {
358
+ // Not even the smallest round distance fits the cell. A missing bar is
359
+ // visibly missing; a bar labelled with a distance it does not span is not.
360
+ return '';
361
+ }
362
+ const actualBarLength = actualTotalLength * mmPerMetre;
363
+ // `heightMm` reaches the emitted `height=` attribute, so it needs the same
364
+ // treatment as the length: NaN printed `height="NaN"` on every rect and
365
+ // `y="NaN"` on both labels, and a negative printed `height="-5.00"`. Guarding
366
+ // one of two attributes that reach the SVG is not guarding.
367
+ const barHeight = Number.isFinite(scaleBar.heightMm)
368
+ ? Math.min(Math.max(scaleBar.heightMm, 0.1), 3)
369
+ : 3;
370
+ // Draw alternating bar segments; `divisions` was normalised above.
272
371
  const divWidth = actualBarLength / divisions;
273
372
  for (let i = 0; i < divisions; i++) {
274
373
  const segX = barX + i * divWidth;
@@ -283,9 +382,10 @@ function renderScaleBarInTitleBlock(scaleBar, scale, x, y, w, h, _hasLogo, effec
283
382
  svg += ` <text x="${barX.toFixed(2)}" y="${labelY.toFixed(2)}" `;
284
383
  svg += `font-family="Arial, sans-serif" font-size="${fontSize}" `;
285
384
  svg += `text-anchor="start" fill="#000000">0</text>\n`;
286
- const endLabel = actualTotalLength < 1
287
- ? `${(actualTotalLength * 100).toFixed(0)}cm`
288
- : `${actualTotalLength.toFixed(0)}m`;
385
+ // Shared with `barLabelIsExact`, which decides whether a length CAN be
386
+ // printed without lying. Those two have to agree, and an inline copy here is
387
+ // exactly how they would stop agreeing.
388
+ const endLabel = formatBarLabel(actualTotalLength);
289
389
  svg += ` <text x="${(barX + actualBarLength).toFixed(2)}" y="${labelY.toFixed(2)}" `;
290
390
  svg += `font-family="Arial, sans-serif" font-size="${fontSize}" `;
291
391
  svg += `text-anchor="end" fill="#000000">${endLabel}</text>\n`;