ooxml-ui 0.31.0 → 1.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 (115) hide show
  1. package/README.md +16 -0
  2. package/custom-elements.json +10 -0
  3. package/dist/{chunk-OXKWNNNJ.js → chunk-3FPW7M6U.js} +1 -1
  4. package/dist/{chunk-XDUYS4KK.js → chunk-BEN3453I.js} +1 -1
  5. package/dist/{chunk-HZ422LTR.js → chunk-BOX43CZD.js} +1 -0
  6. package/dist/{chunk-3GE2NHNG.js → chunk-KVKKXRL7.js} +121 -83
  7. package/dist/{chunk-2QW4QE56.js → chunk-M5DTJSOV.js} +105 -70
  8. package/dist/{chunk-ZV2Z4IWH.js → chunk-USDLALB5.js} +4 -1
  9. package/dist/{chunk-RAZAJMHK.js → chunk-ZU7GX4SY.js} +4 -4
  10. package/dist/controls.js +4 -4
  11. package/dist/docx/hard-break-command.d.ts +1 -3
  12. package/dist/docx/link-commands.d.ts +4 -15
  13. package/dist/docx/localization-strings.d.ts +1 -1
  14. package/dist/docx/localization.d.ts +0 -1
  15. package/dist/docx/note-editor.d.ts +3 -2
  16. package/dist/docx/run-styles.d.ts +1 -1
  17. package/dist/docx/schema.d.ts +1 -1
  18. package/dist/docx/toggle-commands.d.ts +2 -8
  19. package/dist/docx.js +118 -321
  20. package/dist/form/gradient-direction-gallery.d.ts +19 -0
  21. package/dist/form/gradient-gallery-preview.d.ts +3 -0
  22. package/dist/form/gradient-preset-gallery.d.ts +16 -0
  23. package/dist/form/gradient-stop-track.d.ts +20 -0
  24. package/dist/form/number-range.d.ts +12 -0
  25. package/dist/icons.d.ts.map +1 -1
  26. package/dist/icons.js +1 -1
  27. package/dist/index.js +7 -7
  28. package/dist/menu/menu-button.d.ts +10 -0
  29. package/dist/menu/menu-button.d.ts.map +1 -1
  30. package/dist/pptx/{chart-3d-view-scene-IANLDFIE.mjs → chart-3d-view-scene-2KRQ5667.mjs} +2 -2
  31. package/dist/pptx/{chart-3d-view-scene-3DK5TUBZ.cjs → chart-3d-view-scene-36DMZWZ3.cjs} +37 -37
  32. package/dist/pptx/{chunk-C42ZL35K.mjs → chunk-ADOC5JLI.mjs} +1 -1
  33. package/dist/pptx/{chunk-QM4FGZG4.cjs → chunk-AIL3BNZS.cjs} +31 -131
  34. package/dist/pptx/{chunk-WLETAW3T.cjs → chunk-CJU4P5FT.cjs} +33 -13
  35. package/dist/pptx/{chunk-6SCK57RJ.cjs → chunk-KPZWYF44.cjs} +220 -342
  36. package/dist/pptx/{chunk-XQZWZMGZ.cjs → chunk-SM3CWDIQ.cjs} +7 -17
  37. package/dist/pptx/{chunk-JSBGBVBH.mjs → chunk-VRFNAF3Q.mjs} +7 -17
  38. package/dist/pptx/{chunk-EVGSFPUV.mjs → chunk-Y2W4SZER.mjs} +71 -202
  39. package/dist/pptx/{chunk-7JEMQW6R.mjs → chunk-YARCML7O.mjs} +11 -122
  40. package/dist/pptx/{chunk-OHE5UXST.cjs → chunk-YHA6XAB3.cjs} +1 -1
  41. package/dist/pptx/{chunk-X2F6PA2D.mjs → chunk-YOGDIDPB.mjs} +22 -3
  42. package/dist/pptx/dom/index.cjs +238 -238
  43. package/dist/pptx/dom/index.mjs +8 -8
  44. package/dist/pptx/export/html2canvas-clone.cjs +2 -2
  45. package/dist/pptx/export/html2canvas-clone.mjs +1 -1
  46. package/dist/pptx/i18n/index.cjs +6 -6
  47. package/dist/pptx/i18n/index.mjs +1 -1
  48. package/dist/pptx/i18n/translator.d.ts.map +1 -1
  49. package/dist/pptx/index.cjs +3006 -2990
  50. package/dist/pptx/index.mjs +5 -5
  51. package/dist/pptx/render/chart-combo-stock.cjs +3 -3
  52. package/dist/pptx/render/chart-combo-stock.mjs +1 -1
  53. package/dist/pptx/render/chart-manual-layout.d.ts +2 -20
  54. package/dist/pptx/render/chart-manual-layout.d.ts.map +1 -1
  55. package/dist/pptx/render/chart-overlays.cjs +7 -7
  56. package/dist/pptx/render/chart-overlays.mjs +1 -1
  57. package/dist/pptx/render/chart-surface-treemap.cjs +4 -4
  58. package/dist/pptx/render/chart-surface-treemap.mjs +1 -1
  59. package/dist/pptx/render/chart-svg-def-types.d.ts +6 -1
  60. package/dist/pptx/render/chart-svg-def-types.d.ts.map +1 -1
  61. package/dist/pptx/render/chart-view-model-dom-helpers.d.ts +1 -1
  62. package/dist/pptx/render/chart-view-model-dom-helpers.d.ts.map +1 -1
  63. package/dist/pptx/render/chart-view-model.cjs +41 -41
  64. package/dist/pptx/render/chart-view-model.mjs +1 -1
  65. package/dist/pptx/render/chart-waterfall-map.cjs +6 -6
  66. package/dist/pptx/render/chart-waterfall-map.mjs +1 -1
  67. package/dist/pptx/render/connector-elbow-geometry.d.ts +7 -43
  68. package/dist/pptx/render/connector-elbow-geometry.d.ts.map +1 -1
  69. package/dist/pptx/render/element-interaction.d.ts +1 -1
  70. package/dist/pptx/render/element-interaction.d.ts.map +1 -1
  71. package/dist/pptx/render/gradient-picker.d.ts.map +1 -1
  72. package/dist/pptx/render/index.d.ts +2 -0
  73. package/dist/pptx/render/index.d.ts.map +1 -1
  74. package/dist/pptx/render/path-gradient-rect.d.ts +1 -75
  75. package/dist/pptx/render/path-gradient-rect.d.ts.map +1 -1
  76. package/dist/pptx/render/rotate-handle-placement.d.ts +1 -27
  77. package/dist/pptx/render/rotate-handle-placement.d.ts.map +1 -1
  78. package/dist/pptx/render/rotation-drag.d.ts +1 -7
  79. package/dist/pptx/render/rotation-drag.d.ts.map +1 -1
  80. package/dist/pptx/render/viewer-root-options.d.ts +46 -0
  81. package/dist/pptx/render/viewer-root-options.d.ts.map +1 -0
  82. package/dist/pptx/web-components/registration-contract.d.ts.map +1 -1
  83. package/dist/presence.js +2 -2
  84. package/dist/registry.d.ts +2 -0
  85. package/dist/registry.d.ts.map +1 -1
  86. package/dist/smartart.js +2 -2
  87. package/dist/teams/app/files-panel.d.ts +8 -3
  88. package/dist/teams/app/teams-app.d.ts +1 -0
  89. package/dist/teams/app/teams-settings.d.ts +6 -0
  90. package/dist/teams.js +5 -5
  91. package/dist/visio/render-fill.d.ts +2 -1
  92. package/dist/visio/render-svg.d.ts +2 -0
  93. package/dist/visio/ribbon-action.d.ts +8 -1
  94. package/dist/visio/ribbon-parts.d.ts +2 -1
  95. package/dist/visio/ribbon-router.d.ts +4 -2
  96. package/dist/visio/viewer-commands.d.ts +3 -3
  97. package/dist/visio/viewer-draw-tool.d.ts +16 -6
  98. package/dist/visio/viewer-handle-events.d.ts +19 -0
  99. package/dist/visio/viewer-line-endpoints.d.ts +14 -0
  100. package/dist/visio/viewer-rotation-handle.d.ts +14 -0
  101. package/dist/visio/viewer-rotation-preview.d.ts +7 -0
  102. package/dist/visio.js +742 -90
  103. package/dist/xlsx/chart-area-pane.d.ts +9 -0
  104. package/dist/xlsx/chart-fill-binding.d.ts +23 -0
  105. package/dist/xlsx/chart-gradient-preview.d.ts +8 -0
  106. package/dist/xlsx/chart-series-fill.d.ts +2 -1
  107. package/dist/xlsx/chart-series-gradient-type.d.ts +9 -0
  108. package/dist/xlsx/chart-series-gradient.d.ts +8 -0
  109. package/dist/xlsx/chart-series-pane.d.ts +2 -1
  110. package/dist/xlsx/chart-series-range-preview.d.ts +7 -0
  111. package/dist/xlsx/chart-series-transparency.d.ts +2 -1
  112. package/dist/xlsx/grid/measure.d.ts +5 -0
  113. package/dist/xlsx/grid/styles.d.ts +1 -1
  114. package/dist/xlsx.js +965 -105
  115. package/package.json +3 -3
package/dist/visio.js CHANGED
@@ -1,11 +1,12 @@
1
- import { attachKeyTips, registerControls, readOfficeProfile, sanitizeOfficeProfile, writeOfficeProfile } from './chunk-2QW4QE56.js';
2
- import { getIcon, registerIcon } from './chunk-HZ422LTR.js';
3
- import { definePresence } from './chunk-OXKWNNNJ.js';
4
- import './chunk-ZV2Z4IWH.js';
1
+ import { attachKeyTips, registerControls, readOfficeProfile, sanitizeOfficeProfile, writeOfficeProfile } from './chunk-M5DTJSOV.js';
2
+ import { getIcon, registerIcon } from './chunk-BOX43CZD.js';
3
+ import { definePresence } from './chunk-3FPW7M6U.js';
4
+ import './chunk-USDLALB5.js';
5
5
  import './chunk-JFJXBYRD.js';
6
- import { FOREIGN_VECTOR_SCENE_LIMITS, EMPTY_TEXT_SEARCH, EMPTY_EDIT_STATE, assertViewableDocument, indexDocumentText, searchDocumentText, validateSearchQuery, MAX_INPUT_BYTES, DocumentHistory, snapshotEdits, ForeignVectorBudget, VISIO_IDENTITY_TRANSFORM, composeVisioTransform, compatibilityNotes, compatibilityText, visioGradientInstances, visioRenderedGradientStopCount, inspectForeignVectorResource, copySnapshotScene, editErrorMessage, eventKeys, propertyKeys, TEXT_SEARCH_LIMITS, isEditCancellation, visioPageInsertCommand, textSearchStatus, visioFillPatternTransform, visioRenderedGradientStops, visioPageEditToDrawing } from 'ooxml-core/visio/ui';
6
+ import { FOREIGN_VECTOR_SCENE_LIMITS, EMPTY_TEXT_SEARCH, EMPTY_EDIT_STATE, assertViewableDocument, indexDocumentText, searchDocumentText, validateSearchQuery, MAX_INPUT_BYTES, DocumentHistory, snapshotEdits, ForeignVectorBudget, VISIO_IDENTITY_TRANSFORM, composeVisioTransform, compatibilityNotes, compatibilityText, visioGradientInstances, visioRenderedGradientStopCount, inspectForeignVectorResource, copySnapshotScene, editErrorMessage, eventKeys, propertyKeys, TEXT_SEARCH_LIMITS, isEditCancellation, visioPageInsertCommand, visioQuarterTurnCommand, visioLocalRotationShape, visioStraightLineHandles, visioPageEditToDrawing, textSearchStatus, visioRotationPreviewTransform, visioFillPatternTransform, visioRenderedGradientStops, visioStrokeGradient, visioNextShapeId } from 'ooxml-core/visio/ui';
7
7
  export { TEXT_SEARCH_LIMITS, compatibilityNotes, compatibilityText, demoDocument, eventKeys, propertyKeys } from 'ooxml-core/visio/ui';
8
8
  import { VISIO_VISIBILITY_LIMITS, loadVisio, getVisioPageLayers, resolveVisioPageVisibility, visioFilledArrow, layoutVisioFilledArrowLine, visioOpenArrowPath } from 'ooxml-core/visio';
9
+ import { resolveRotateHandlePlacement, createRotationDrag } from 'ooxml-core/geometry';
9
10
 
10
11
  function createWorkerEditor(factory, timeoutMs = 2e4) {
11
12
  const create = (() => {
@@ -1043,10 +1044,10 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
1043
1044
  var backstage_default = "/* Visio's File tab and backstage. */\n:host {\n position: relative;\n}\n/* The shared office-ui-backstage draws the File column; these style Visio's page content. */\n.backstage {\n --office-accent: var(--_vv-accent);\n --office-accent-foreground: var(--vv-accent-ink, #fff);\n}\n.backstage-page h1 {\n margin: 0 0 24px;\n font-size: 34px;\n font-weight: 300;\n}\n.backstage-page h2 {\n margin: 20px 0 8px;\n font-size: 16px;\n font-weight: 600;\n}\n.backstage-file {\n margin-top: 0 !important;\n font-size: 20px !important;\n font-weight: 400 !important;\n overflow-wrap: anywhere;\n}\n.backstage-facts {\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 8px 24px;\n max-width: 520px;\n margin: 0;\n font-size: 13px;\n}\n.backstage-facts dt {\n color: var(--_vv-muted);\n}\n.backstage-facts dd {\n margin: 0;\n overflow-wrap: anywhere;\n}\n.backstage-action {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 2px;\n width: min(420px, 100%);\n min-height: 64px;\n margin: 0 0 10px;\n padding: 10px 16px;\n text-align: left;\n background: var(--_vv-surface);\n}\n.backstage-action strong {\n font-size: 14px;\n font-weight: 600;\n}\n.backstage-action span,\n.backstage-muted,\n.backstage-note-count {\n color: var(--_vv-muted);\n font-size: 12px;\n}\n.backstage-templates {\n display: flex;\n flex-wrap: wrap;\n gap: 12px;\n}\n.backstage-templates .backstage-action,\n.backstage-templates ::slotted(*) {\n width: 200px;\n min-height: 120px;\n}\n/* Account: the shared profile editor, then Visio's product information. */\n.backstage-page office-ui-account {\n max-width: 560px;\n}\n.backstage-product {\n margin-top: 24px;\n}\n/* Share with people: a live session between windows of this browser. */\n.share-panel {\n max-width: 560px;\n margin-bottom: 16px;\n}\n.share-panel h2 {\n margin-top: 0 !important;\n}\n.share-field {\n display: grid;\n gap: 4px;\n max-width: 320px;\n margin: 12px 0;\n font-size: 13px;\n}\n.share-room {\n min-height: 30px;\n padding: 2px 8px;\n border: 1px solid var(--_vv-border);\n border-radius: 3px;\n background: var(--_vv-surface);\n color: inherit;\n font: inherit;\n}\n.share-buttons button {\n min-height: 32px;\n padding: 0 16px;\n}\n.share-buttons button[hidden] {\n display: none;\n}\n.share-status {\n min-height: 20px;\n font-size: 13px;\n}\n";
1044
1045
 
1045
1046
  // raw-css:/home/runner/work/ooxml/ooxml/src/ui/src/visio/styles/canvas.css?raw
1046
- var canvas_default = "/*\n * Drawing canvas focus. SVG outlines are drawn in drawing units (inches here), so a browser\n * focus ring on a shape becomes a page-sized blob. Shapes show focus with a dashed accent\n * stroke instead, only for keyboard focus; selection keeps its solid accent stroke.\n */\n.paper [data-shape-id]:focus,\n.paper [data-shape-id]:focus-visible {\n outline: none;\n}\n.paper [data-shape-id]:focus-visible > [data-geometry] {\n stroke: var(--vv-focus, var(--_vv-accent)) !important;\n stroke-width: 0.04 !important;\n stroke-dasharray: 0.08 0.05;\n}\n";
1047
+ var canvas_default = "/*\n * Drawing canvas focus. SVG outlines are drawn in drawing units (inches here), so a browser\n * focus ring on a shape becomes a page-sized blob. Shapes show focus with a dashed accent\n * stroke instead, only for keyboard focus; selection keeps its solid accent stroke.\n */\n.paper [data-shape-id]:focus,\n.paper [data-shape-id]:focus-visible {\n outline: none;\n}\n.paper [data-shape-id]:focus-visible > [data-geometry] {\n stroke: var(--vv-focus, var(--_vv-accent)) !important;\n stroke-width: 0.04 !important;\n stroke-dasharray: 0.08 0.05;\n}\n\n.paper [data-line-endpoint],\n.rotation-overlay [data-rotation-handle] {\n fill: var(--_vv-accent);\n stroke: white;\n stroke-width: 1.5px;\n cursor: crosshair;\n touch-action: none;\n pointer-events: all;\n}\n.paper .endpoint-preview,\n.paper .rotation-preview,\n.rotation-overlay .rotation-stem {\n stroke: var(--_vv-accent);\n stroke-width: 1.5px;\n stroke-dasharray: 5 3;\n pointer-events: none;\n}\n\n.rotation-overlay {\n position: absolute;\n z-index: 1;\n pointer-events: none;\n}\n";
1047
1048
 
1048
1049
  // raw-css:/home/runner/work/ooxml/ooxml/src/ui/src/visio/styles/ribbon.css?raw
1049
- var ribbon_default = "/*\n * Microsoft Visio ribbon layout on the shared ooxml-ui controls. Viewer theme tokens feed the\n * shared --office-* tokens, so host themes still apply. Loaded after the legacy styles.ts.\n */\n:host {\n --office-foreground: var(--_vv-ink);\n --office-muted-foreground: var(--_vv-muted);\n --office-background: var(--_vv-surface);\n --office-surface: var(--_vv-accent-soft);\n --office-border: var(--_vv-border);\n --office-accent: var(--_vv-accent);\n --office-accent-foreground: var(--vv-accent-ink, #fff);\n --office-ring: var(--vv-focus, var(--_vv-accent));\n --office-font: 'Segoe UI', Arial, sans-serif;\n}\n\n/* Tab row: Quick Access Toolbar, then Visio's tabs. */\n.ribbon-head {\n display: flex;\n align-items: stretch;\n min-width: 0;\n border-bottom: 1px solid var(--_vv-border);\n}\n.qat {\n display: flex;\n align-items: center;\n gap: 0;\n padding: 0 6px;\n border-right: 1px solid var(--_vv-border);\n}\n.qat office-ui-button {\n --office-target-size: 30px;\n}\n.ribbon-head .ribbon-tabs {\n flex: 0 1 auto;\n min-width: 0;\n height: 36px;\n border-bottom: 0;\n overflow-x: auto;\n scrollbar-width: none;\n}\n.ribbon-head .ribbon-tabs button {\n flex: none;\n min-height: 35px;\n padding: 6px 12px;\n font-size: 13px;\n}\n\n/* Panels: groups of large commands and three-row stacks, as dense as Visio's. */\n.ribbon-content {\n min-height: 96px;\n padding: 2px 2px 0;\n}\n.ribbon-row {\n display: flex;\n flex-wrap: nowrap;\n align-items: stretch;\n gap: 0;\n padding: 0;\n border: 0;\n background: transparent;\n}\n.ribbon-row office-ui-ribbon-group,\n.tools-content office-ui-ribbon-group {\n min-width: 0;\n}\noffice-ui-ribbon-group office-ui-button,\noffice-ui-ribbon-group office-ui-menu-button {\n --office-target-size: 24px;\n}\noffice-ui-ribbon-group [data-size='large'] {\n --office-target-size: 30px;\n}\noffice-ui-button[variant='stacked']::part(button) {\n min-width: 48px;\n min-height: 66px;\n padding: 2px 6px;\n font-size: 12px;\n}\noffice-ui-menu-button[variant='stacked']::part(button) {\n min-width: 48px;\n min-height: 50px;\n font-size: 12px;\n}\noffice-ui-ribbon-group [data-size='small']::part(button) {\n justify-content: flex-start;\n font-size: 12px;\n}\noffice-ui-ribbon-stack:not([orientation]) {\n justify-content: flex-start;\n padding-top: 2px;\n}\noffice-ui-ribbon-stack:not([orientation]) > * {\n min-height: 24px;\n}\noffice-ui-ribbon-group > office-ui-ribbon-stack > office-ui-ribbon-stack[orientation] {\n min-height: 32px;\n}\noffice-ui-select[data-combo] {\n --office-target-size: 24px;\n}\noffice-ui-menu-button[data-active]::part(button) {\n background: var(--_vv-accent-soft);\n box-shadow: inset 0 0 0 1px var(--_vv-accent);\n}\n[data-unsupported] {\n cursor: not-allowed;\n}\n.ribbon-check {\n display: flex;\n align-items: center;\n gap: 6px;\n min-height: 24px;\n padding: 0 6px;\n font-size: 12px;\n white-space: nowrap;\n cursor: pointer;\n}\n.ribbon-check[data-unsupported] {\n color: var(--_vv-muted);\n opacity: 0.6;\n}\n.ribbon-hint {\n display: none;\n}\n\n/* Drawing window with optional rulers. */\n.canvas-area {\n display: grid;\n flex: 1;\n min-width: 0;\n min-height: 0;\n grid-template-columns: 0 1fr;\n grid-template-rows: 0 1fr;\n}\n.canvas-area[data-ruler='true'] {\n grid-template-columns: 18px 1fr;\n grid-template-rows: 18px 1fr;\n}\n.canvas-area > .viewport {\n grid-column: 2;\n grid-row: 2;\n}\n.ruler,\n.ruler-corner {\n display: none;\n background: var(--_vv-surface);\n}\n.canvas-area[data-ruler='true'] > .ruler,\n.canvas-area[data-ruler='true'] > .ruler-corner {\n display: block;\n}\n.ruler-corner {\n grid-column: 1;\n grid-row: 1;\n border-right: 1px solid var(--_vv-border);\n border-bottom: 1px solid var(--_vv-border);\n}\n/* The shared office-ui-ruler draws ticks, labels and its own edge. */\n.ruler-h {\n grid-column: 2;\n grid-row: 1;\n}\n.ruler-v {\n grid-column: 1;\n grid-row: 2;\n}\n\n.page-bar {\n display: flex;\n flex: none;\n align-items: stretch;\n min-width: 0;\n border-top: 1px solid var(--_vv-border);\n background: var(--_vv-secondary);\n}\n.page-bar office-ui-menu-button {\n --office-target-size: 28px;\n flex: none;\n padding: 0 2px;\n}\n.page-tabs {\n flex: 1;\n min-width: 0;\n border-top: 0;\n}\noffice-ui-status-bar.status {\n gap: 16px;\n min-height: 29px;\n padding: 0 8px 0 12px;\n font-size: 11px;\n background: var(--_vv-surface);\n color: var(--_vv-muted);\n}\noffice-ui-status-item[value=''] {\n display: none;\n}\noffice-ui-status-item[data-page-status] {\n color: var(--_vv-ink);\n}\n.status-message {\n flex: 1;\n min-width: 0;\n}\n.viewport[data-tool='rectangle'],\n.viewport[data-tool='rectangle'] .paper [data-shape-id] {\n cursor: crosshair;\n}\n.viewport[data-tool='rectangle'] {\n touch-action: none;\n}\n.draw-preview {\n fill: var(--_vv-accent-soft);\n stroke: var(--_vv-accent);\n stroke-width: 1.5px;\n stroke-dasharray: 4 3;\n pointer-events: none;\n}\n.viewport[data-grid='true'] .paper {\n --_vv-grid-minor: rgb(0 0 0 / 7%);\n --_vv-grid-major: rgb(0 0 0 / 16%);\n background-image:\n linear-gradient(to right, var(--_vv-grid-major) 1px, transparent 1px),\n linear-gradient(to top, var(--_vv-grid-major) 1px, transparent 1px),\n linear-gradient(to right, var(--_vv-grid-minor) 1px, transparent 1px),\n linear-gradient(to top, var(--_vv-grid-minor) 1px, transparent 1px);\n background-size:\n var(--vv-inch, 96px) var(--vv-inch, 96px),\n var(--vv-inch, 96px) var(--vv-inch, 96px),\n calc(var(--vv-inch, 96px) / 4) calc(var(--vv-inch, 96px) / 4),\n calc(var(--vv-inch, 96px) / 4) calc(var(--vv-inch, 96px) / 4);\n background-position: left bottom;\n}\n:host(:fullscreen) {\n width: 100vw;\n height: 100vh;\n}\n\n@media (max-width: 760px) {\n .ribbon-row {\n flex: 1;\n min-width: 0;\n }\n .qat office-ui-button {\n --office-target-size: 44px;\n }\n .ribbon-tools .tools-content office-ui-ribbon-group {\n width: 100%;\n }\n office-ui-status-bar.status {\n gap: 4px;\n min-height: 48px;\n }\n office-ui-status-bar .notes-strip,\n office-ui-status-bar .status-message,\n office-ui-status-item[data-shape-status],\n office-ui-status-item[data-page-status] {\n display: none;\n }\n .zoom-controls {\n flex: 1;\n min-width: 0;\n --office-zoom-slider-width: 0;\n }\n}\n\n/* Visio's Tell me box sits right after the tabs. */\n.tell-me {\n flex: 0 1 280px;\n min-width: 140px;\n margin: 0 8px;\n --office-target-size: 28px;\n}\n.presentation-mode {\n --office-target-size: 24px;\n}\n@media (max-width: 760px) {\n .tell-me {\n display: none;\n }\n}\n";
1050
+ var ribbon_default = "/*\n * Microsoft Visio ribbon layout on the shared ooxml-ui controls. Viewer theme tokens feed the\n * shared --office-* tokens, so host themes still apply. Loaded after the legacy styles.ts.\n */\n:host {\n --office-foreground: var(--_vv-ink);\n --office-muted-foreground: var(--_vv-muted);\n --office-background: var(--_vv-surface);\n --office-surface: var(--_vv-accent-soft);\n --office-border: var(--_vv-border);\n --office-accent: var(--_vv-accent);\n --office-accent-foreground: var(--vv-accent-ink, #fff);\n --office-ring: var(--vv-focus, var(--_vv-accent));\n --office-font: 'Segoe UI', Arial, sans-serif;\n}\n\n/* Tab row: Quick Access Toolbar, then Visio's tabs. */\n.ribbon-head {\n display: flex;\n align-items: stretch;\n min-width: 0;\n border-bottom: 1px solid var(--_vv-border);\n}\n.qat {\n display: flex;\n align-items: center;\n gap: 0;\n padding: 0 6px;\n border-right: 1px solid var(--_vv-border);\n}\n.qat office-ui-button {\n --office-target-size: 30px;\n}\n.ribbon-head .ribbon-tabs {\n flex: 0 1 auto;\n min-width: 0;\n height: 36px;\n border-bottom: 0;\n overflow-x: auto;\n scrollbar-width: none;\n}\n.ribbon-head .ribbon-tabs button {\n flex: none;\n min-height: 35px;\n padding: 6px 12px;\n font-size: 13px;\n}\n\n/* Panels: groups of large commands and three-row stacks, as dense as Visio's. */\n.ribbon-content {\n min-height: 96px;\n padding: 2px 2px 0;\n}\n.ribbon-row {\n display: flex;\n flex-wrap: nowrap;\n align-items: stretch;\n gap: 0;\n padding: 0;\n border: 0;\n background: transparent;\n}\n.ribbon-row office-ui-ribbon-group,\n.tools-content office-ui-ribbon-group {\n min-width: 0;\n}\noffice-ui-ribbon-group office-ui-button,\noffice-ui-ribbon-group office-ui-menu-button {\n --office-target-size: 24px;\n}\noffice-ui-ribbon-group [data-size='large'] {\n --office-target-size: 30px;\n}\noffice-ui-button[variant='stacked']::part(button) {\n min-width: 48px;\n min-height: 66px;\n padding: 2px 6px;\n font-size: 12px;\n}\noffice-ui-menu-button[variant='stacked']::part(button) {\n min-width: 48px;\n min-height: 50px;\n font-size: 12px;\n}\noffice-ui-ribbon-group [data-size='small']::part(button) {\n justify-content: flex-start;\n font-size: 12px;\n}\noffice-ui-ribbon-stack:not([orientation]) {\n justify-content: flex-start;\n padding-top: 2px;\n}\noffice-ui-ribbon-stack:not([orientation]) > * {\n min-height: 24px;\n}\noffice-ui-ribbon-group > office-ui-ribbon-stack > office-ui-ribbon-stack[orientation] {\n min-height: 32px;\n}\noffice-ui-select[data-combo] {\n --office-target-size: 24px;\n}\noffice-ui-menu-button[data-active]::part(button) {\n background: var(--_vv-accent-soft);\n box-shadow: inset 0 0 0 1px var(--_vv-accent);\n}\n[data-unsupported] {\n cursor: not-allowed;\n}\n.ribbon-check {\n display: flex;\n align-items: center;\n gap: 6px;\n min-height: 24px;\n padding: 0 6px;\n font-size: 12px;\n white-space: nowrap;\n cursor: pointer;\n}\n.ribbon-check[data-unsupported] {\n color: var(--_vv-muted);\n opacity: 0.6;\n}\n.ribbon-hint {\n display: none;\n}\n\n/* Drawing window with optional rulers. */\n.canvas-area {\n display: grid;\n flex: 1;\n min-width: 0;\n min-height: 0;\n grid-template-columns: 0 1fr;\n grid-template-rows: 0 1fr;\n}\n.canvas-area[data-ruler='true'] {\n grid-template-columns: 18px 1fr;\n grid-template-rows: 18px 1fr;\n}\n.canvas-area > .viewport {\n grid-column: 2;\n grid-row: 2;\n}\n.ruler,\n.ruler-corner {\n display: none;\n background: var(--_vv-surface);\n}\n.canvas-area[data-ruler='true'] > .ruler,\n.canvas-area[data-ruler='true'] > .ruler-corner {\n display: block;\n}\n.ruler-corner {\n grid-column: 1;\n grid-row: 1;\n border-right: 1px solid var(--_vv-border);\n border-bottom: 1px solid var(--_vv-border);\n}\n/* The shared office-ui-ruler draws ticks, labels and its own edge. */\n.ruler-h {\n grid-column: 2;\n grid-row: 1;\n}\n.ruler-v {\n grid-column: 1;\n grid-row: 2;\n}\n\n.page-bar {\n display: flex;\n flex: none;\n align-items: stretch;\n min-width: 0;\n border-top: 1px solid var(--_vv-border);\n background: var(--_vv-secondary);\n}\n.page-bar office-ui-menu-button {\n --office-target-size: 28px;\n flex: none;\n padding: 0 2px;\n}\n.page-tabs {\n flex: 1;\n min-width: 0;\n border-top: 0;\n}\noffice-ui-status-bar.status {\n gap: 16px;\n min-height: 29px;\n padding: 0 8px 0 12px;\n font-size: 11px;\n background: var(--_vv-surface);\n color: var(--_vv-muted);\n}\noffice-ui-status-item[value=''] {\n display: none;\n}\noffice-ui-status-item[data-page-status] {\n color: var(--_vv-ink);\n}\n.status-message {\n flex: 1;\n min-width: 0;\n}\n.viewport[data-tool='ellipse'],\n.viewport[data-tool='ellipse'] .paper [data-shape-id],\n.viewport[data-tool='rectangle'],\n.viewport[data-tool='rectangle'] .paper [data-shape-id],\n.viewport[data-tool='line'],\n.viewport[data-tool='line'] .paper [data-shape-id] {\n cursor: crosshair;\n}\n.viewport[data-tool='ellipse'],\n.viewport[data-tool='rectangle'],\n.viewport[data-tool='line'] {\n touch-action: none;\n}\n.draw-preview {\n fill: var(--_vv-accent-soft);\n stroke: var(--_vv-accent);\n stroke-width: 1.5px;\n stroke-dasharray: 4 3;\n pointer-events: none;\n}\n.viewport[data-grid='true'] .paper {\n --_vv-grid-minor: rgb(0 0 0 / 7%);\n --_vv-grid-major: rgb(0 0 0 / 16%);\n background-image:\n linear-gradient(to right, var(--_vv-grid-major) 1px, transparent 1px),\n linear-gradient(to top, var(--_vv-grid-major) 1px, transparent 1px),\n linear-gradient(to right, var(--_vv-grid-minor) 1px, transparent 1px),\n linear-gradient(to top, var(--_vv-grid-minor) 1px, transparent 1px);\n background-size:\n var(--vv-inch, 96px) var(--vv-inch, 96px),\n var(--vv-inch, 96px) var(--vv-inch, 96px),\n calc(var(--vv-inch, 96px) / 4) calc(var(--vv-inch, 96px) / 4),\n calc(var(--vv-inch, 96px) / 4) calc(var(--vv-inch, 96px) / 4);\n background-position: left bottom;\n}\n:host(:fullscreen) {\n width: 100vw;\n height: 100vh;\n}\n\n@media (max-width: 760px) {\n .ribbon-row {\n flex: 1;\n min-width: 0;\n }\n .qat office-ui-button {\n --office-target-size: 44px;\n }\n .ribbon-tools .tools-content office-ui-ribbon-group {\n width: 100%;\n }\n office-ui-status-bar.status {\n gap: 4px;\n min-height: 48px;\n }\n office-ui-status-bar .notes-strip,\n office-ui-status-bar .status-message,\n office-ui-status-item[data-shape-status],\n office-ui-status-item[data-page-status] {\n display: none;\n }\n .zoom-controls {\n flex: 1;\n min-width: 0;\n --office-zoom-slider-width: 0;\n }\n}\n\n/* Visio's Tell me box sits right after the tabs. */\n.tell-me {\n flex: 0 1 280px;\n min-width: 140px;\n margin: 0 8px;\n --office-target-size: 28px;\n}\n.presentation-mode {\n --office-target-size: 24px;\n}\n@media (max-width: 760px) {\n .tell-me {\n display: none;\n }\n}\n";
1050
1051
 
1051
1052
  // raw-css:/home/runner/work/ooxml/ooxml/src/ui/src/visio/styles/shapes.css?raw
1052
1053
  var shapes_default = "/* Visio's Shapes window: stencil views, More/Quick Shapes rows and a two-column master grid. */\n.shapes-pane {\n display: flex;\n flex: 0 0 232px;\n flex-direction: column;\n width: 232px;\n min-width: 0;\n overflow: hidden;\n border-right: 1px solid var(--_vv-border);\n background: var(--_vv-secondary);\n}\n.shapes-pane .pane-heading {\n height: 42px;\n font-size: 18px;\n font-weight: 400;\n color: var(--_vv-accent);\n}\n.pane-collapse {\n min-height: 28px;\n width: 28px;\n padding: 0;\n border-color: transparent;\n background: transparent;\n font-size: 18px;\n line-height: 1;\n}\n.shapes-views {\n display: flex;\n gap: 14px;\n padding: 4px 12px 0;\n border-bottom: 1px solid var(--_vv-border);\n}\n.shapes-views button {\n position: relative;\n min-height: 28px;\n padding: 2px 0 6px;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: var(--_vv-muted);\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n}\n.shapes-views button[aria-selected='true'] {\n color: var(--_vv-ink);\n}\n.shapes-views button[aria-selected='true']::after {\n content: '';\n position: absolute;\n inset: auto 0 -1px;\n height: 2px;\n background: var(--_vv-accent);\n}\n#shapes-stencils,\n#shapes-search {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 4px 0 8px;\n}\n.shapes-row {\n display: flex;\n align-items: center;\n width: 100%;\n min-height: 30px;\n padding: 0 12px;\n border: 0;\n border-radius: 0;\n background: transparent;\n font-size: 12px;\n text-align: left;\n}\n.shapes-row::after {\n content: '\u203A';\n margin-left: auto;\n font-size: 15px;\n}\n.stencil-title {\n margin: 2px 0;\n padding: 6px 12px;\n border-left: 3px solid var(--_vv-accent);\n background: var(--_vv-accent-soft);\n font-size: 12px;\n font-weight: 600;\n}\n.masters {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2px;\n margin: 0;\n padding: 4px 6px;\n list-style: none;\n}\n.masters li[hidden] {\n display: none;\n}\n.master {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 40px;\n padding: 4px 6px;\n border-color: transparent;\n background: transparent;\n font-size: 11px;\n line-height: 1.2;\n text-align: left;\n cursor: grab;\n}\n.master svg {\n flex: none;\n width: 28px;\n height: 28px;\n fill: var(--_vv-surface);\n stroke: var(--_vv-accent);\n stroke-width: 1.2;\n stroke-linejoin: round;\n}\n.master[data-unsupported] {\n cursor: not-allowed;\n opacity: 0.45;\n}\n.master[data-unsupported]:hover {\n border-color: transparent;\n background: transparent;\n}\n.shapes-search-field {\n display: block;\n width: calc(100% - 24px);\n margin: 6px 12px;\n}\n.shapes-empty {\n margin: 8px 12px;\n color: var(--_vv-muted);\n font-size: 12px;\n}\n@media (max-width: 760px) {\n .shapes-pane {\n position: absolute;\n inset: 0 auto 0 0;\n z-index: 3;\n width: min(260px, 100%);\n box-shadow: 8px 0 24px #0002;\n }\n .master {\n min-height: 44px;\n }\n}\n.shapes-strip {\n display: flex;\n flex: 0 0 28px;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n width: 28px;\n min-width: 28px;\n padding: 8px 0;\n border: 0;\n border-right: 1px solid var(--_vv-border);\n border-radius: 0;\n background: var(--_vv-secondary);\n color: var(--_vv-accent);\n font-size: 16px;\n}\n.shapes-strip[hidden] {\n display: none;\n}\n.shapes-strip-label {\n writing-mode: vertical-rl;\n color: var(--_vv-ink);\n font-size: 12px;\n}\n/* Visio's floating Pan & Zoom window. */\n.pan-zoom {\n position: absolute;\n right: 16px;\n bottom: 16px;\n z-index: 5;\n width: 240px;\n border: 1px solid var(--_vv-border);\n background: var(--_vv-surface);\n box-shadow: 0 6px 20px rgb(0 0 0 / 18%);\n}\n.pan-zoom[hidden] {\n display: none;\n}\n.pan-zoom-heading {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 2px 4px 2px 10px;\n border-bottom: 1px solid var(--_vv-border);\n font-size: 12px;\n font-weight: 600;\n}\n.pan-zoom-close {\n min-height: 24px;\n width: 24px;\n padding: 0;\n border-color: transparent;\n background: transparent;\n}\n.pan-zoom-view {\n display: grid;\n place-items: center;\n height: 180px;\n padding: 8px;\n background: var(--_vv-bg);\n cursor: crosshair;\n touch-action: none;\n}\n.pan-zoom-view svg {\n max-width: 100%;\n max-height: 100%;\n background: #fff;\n box-shadow: 0 1px 3px rgb(0 0 0 / 20%);\n}\n.pan-zoom-frame {\n fill: rgb(220 38 38 / 8%);\n stroke: #dc2626;\n stroke-width: 2px;\n}\n";
@@ -1088,13 +1089,14 @@ function command(doc, spec) {
1088
1089
  });
1089
1090
  return el2;
1090
1091
  }
1091
- function menu(doc, spec) {
1092
+ function menu(doc, spec, submenu = false) {
1092
1093
  const el2 = doc.createElement("office-ui-menu-button");
1093
1094
  const actions = /* @__PURE__ */ new Map();
1094
1095
  const { action: _main, ...dropdown2 } = spec;
1095
1096
  const reason = spec.unsupported ?? spec.items.find((item) => item.unsupported)?.unsupported;
1096
1097
  decorate(el2, spec.split ? spec : { ...dropdown2, ...reason ? { unsupported: reason } : {} });
1097
- if (spec.split ? spec.action : spec.items.some((item) => item.action)) {
1098
+ const usable = (item) => !!item.action || !!item.items?.some(usable);
1099
+ if (spec.split ? spec.action : spec.items.some(usable)) {
1098
1100
  el2.removeAttribute("disabled");
1099
1101
  delete el2.dataset.unsupported;
1100
1102
  el2.setAttribute("title", spec.label);
@@ -1104,18 +1106,27 @@ function menu(doc, spec) {
1104
1106
  if (spec.size === "icon") el2.setAttribute("icon-only", "");
1105
1107
  el2.dataset.size = spec.size ?? "large";
1106
1108
  el2.dataset.menu = spec.id;
1109
+ if (submenu) {
1110
+ el2.setAttribute("submenu", "");
1111
+ el2.removeAttribute("variant");
1112
+ }
1107
1113
  if (spec.split) actions.set(spec.id, spec.action);
1108
- for (const item of spec.items) {
1109
- const entry = doc.createElement("office-ui-menu-item");
1110
- decorate(entry, item);
1114
+ const collect = (item) => {
1111
1115
  actions.set(item.id, item.action);
1116
+ item.items?.forEach(collect);
1117
+ };
1118
+ for (const item of spec.items) {
1119
+ const entry = item.items ? menu(doc, { ...item, items: item.items }, true) : doc.createElement("office-ui-menu-item");
1120
+ if (!item.items) decorate(entry, item);
1121
+ collect(item);
1112
1122
  el2.append(entry);
1113
1123
  }
1114
- el2.addEventListener("office-command", (event) => {
1115
- event.stopPropagation();
1116
- const action2 = actions.get(event.detail.command);
1117
- if (action2) emitRibbonAction(el2, action2);
1118
- });
1124
+ if (!submenu)
1125
+ el2.addEventListener("office-command", (event) => {
1126
+ event.stopPropagation();
1127
+ const action2 = actions.get(event.detail.command);
1128
+ if (action2) emitRibbonAction(el2, action2);
1129
+ });
1119
1130
  return el2;
1120
1131
  }
1121
1132
  function stack(doc, children, horizontal = false) {
@@ -1417,7 +1428,7 @@ var TEXT2 = "Needs core text formatting edits.";
1417
1428
  var STYLE2 = "Needs core fill, line and effect edits.";
1418
1429
  var ARRANGE = "Needs core alignment, position, z-order and grouping edits.";
1419
1430
  var CONNECT = "Needs core connector and glue edits.";
1420
- var SHAPES = "Needs core shape creation beyond rectangles.";
1431
+ var SHAPES = "This shape type cannot be created yet.";
1421
1432
  var SELECT = "Needs multi-shape selection.";
1422
1433
  var small2 = (spec) => ({ size: "small", ...spec });
1423
1434
  var icon2 = (spec) => ({ size: "icon", ...spec });
@@ -1459,8 +1470,21 @@ function buildHomePanel(doc, panel) {
1459
1470
  action: { type: "tool", tool: "rectangle" },
1460
1471
  checked: false
1461
1472
  },
1462
- { id: "ellipse", label: "Ellipse", unsupported: SHAPES },
1463
- { id: "line-tool", label: "Line", icon: "line", unsupported: SHAPES },
1473
+ {
1474
+ id: "ellipse",
1475
+ label: "Ellipse",
1476
+ action: { type: "tool", tool: "ellipse" },
1477
+ keys: ["Control+9", "Ctrl+9"],
1478
+ checked: false
1479
+ },
1480
+ {
1481
+ id: "line-tool",
1482
+ label: "Line",
1483
+ icon: "line",
1484
+ action: { type: "tool", tool: "line" },
1485
+ keys: ["Control+6", "Ctrl+6"],
1486
+ checked: false
1487
+ },
1464
1488
  { id: "freeform", label: "Freeform", unsupported: SHAPES },
1465
1489
  { id: "arc", label: "Arc", unsupported: SHAPES },
1466
1490
  { id: "pencil", label: "Pencil", icon: "pencil", unsupported: SHAPES }
@@ -1547,7 +1571,32 @@ function buildHomePanel(doc, panel) {
1547
1571
  unsupported: ARRANGE,
1548
1572
  items: [
1549
1573
  { id: "auto-align", label: "Auto Align & Space", unsupported: ARRANGE },
1550
- { id: "rotate", label: "Rotate Shapes", unsupported: ARRANGE }
1574
+ {
1575
+ id: "rotate",
1576
+ label: "Rotate Shapes",
1577
+ items: [
1578
+ {
1579
+ id: "rotate-left",
1580
+ label: "Rotate Left 90\xB0",
1581
+ action: { type: "rotate", direction: "left" }
1582
+ },
1583
+ {
1584
+ id: "rotate-right",
1585
+ label: "Rotate Right 90\xB0",
1586
+ action: { type: "rotate", direction: "right" }
1587
+ },
1588
+ {
1589
+ id: "flip-horizontal",
1590
+ label: "Flip Horizontal",
1591
+ action: { type: "flip", axis: "horizontal" }
1592
+ },
1593
+ {
1594
+ id: "flip-vertical",
1595
+ label: "Flip Vertical",
1596
+ action: { type: "flip", axis: "vertical" }
1597
+ }
1598
+ ]
1599
+ }
1551
1600
  ]
1552
1601
  }),
1553
1602
  stack(doc, [
@@ -3800,44 +3849,50 @@ function same(a, b) {
3800
3849
  var SNAP = 1 / 16;
3801
3850
  var MIN_SIZE = SNAP;
3802
3851
  var snap = (value) => Math.round(value / SNAP) * SNAP;
3803
- function nextShapeId(page2) {
3804
- let max = 0;
3805
- const pending = [...page2.shapes];
3806
- while (pending.length) {
3807
- const shape = pending.pop();
3808
- if (/^\d{1,9}$/.test(shape.id)) max = Math.max(max, Number(shape.id));
3809
- pending.push(...shape.children);
3810
- }
3811
- return String(max + 1);
3812
- }
3813
- function pagePoint(svg, page2, event) {
3852
+ function pagePoint(svg, page2, event, options = {}) {
3814
3853
  const matrix2 = svg.getScreenCTM?.();
3815
3854
  if (!matrix2) return void 0;
3816
3855
  const point = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix2.inverse());
3817
- return {
3818
- x: snap(Math.max(0, Math.min(page2.width, point.x))),
3819
- y: snap(Math.max(0, Math.min(page2.height, point.y)))
3856
+ const coordinate = (value, maximum) => {
3857
+ const bounded = options.bounded === false ? value : Math.max(0, Math.min(maximum, value));
3858
+ return options.snap === false ? bounded : snap(bounded);
3820
3859
  };
3860
+ return { x: coordinate(point.x, page2.width), y: coordinate(point.y, page2.height) };
3821
3861
  }
3822
3862
  async function insertRectangle(controller, page2, centre, size) {
3823
- const shapeId = nextShapeId(page2);
3824
- const pageId = page2.id;
3825
- await controller.applyEdits([
3826
- visioPageEditToDrawing(page2, {
3827
- type: "create-rectangle",
3828
- pageId,
3829
- shapeId,
3830
- x: centre.x,
3831
- y: page2.height - centre.y,
3832
- width: size.width,
3833
- height: size.height
3834
- })
3835
- ]);
3863
+ return insertBox(controller, page2, centre, size, "rectangle");
3864
+ }
3865
+ async function insertBox(controller, page2, centre, size, kind) {
3866
+ const shapeId = visioNextShapeId(page2);
3867
+ return insertGeometry(controller, page2, {
3868
+ type: kind === "ellipse" ? "create-ellipse" : "create-rectangle",
3869
+ pageId: page2.id,
3870
+ shapeId,
3871
+ x: centre.x,
3872
+ y: page2.height - centre.y,
3873
+ width: size.width,
3874
+ height: size.height
3875
+ });
3876
+ }
3877
+ async function insertLine(controller, page2, begin, end) {
3878
+ return insertGeometry(controller, page2, {
3879
+ type: "create-line",
3880
+ pageId: page2.id,
3881
+ shapeId: visioNextShapeId(page2),
3882
+ beginX: begin.x,
3883
+ beginY: page2.height - begin.y,
3884
+ endX: end.x,
3885
+ endY: page2.height - end.y
3886
+ });
3887
+ }
3888
+ async function insertGeometry(controller, page2, command2) {
3889
+ const { shapeId, pageId } = command2;
3890
+ await controller.applyEdits([visioPageEditToDrawing(page2, command2)]);
3836
3891
  const created = controller.state.document?.pages.find((candidate) => candidate.id === pageId)?.shapes.find((shape) => shape.id === shapeId);
3837
3892
  if (created) controller.selectShape({ id: shapeId, name: created.name, pageId });
3838
3893
  return shapeId;
3839
3894
  }
3840
- var RectangleDrawTool = class {
3895
+ var ShapeDrawTool = class {
3841
3896
  constructor(viewport, controller, options) {
3842
3897
  this.viewport = viewport;
3843
3898
  this.controller = controller;
@@ -3859,7 +3914,7 @@ var RectangleDrawTool = class {
3859
3914
  viewport.addEventListener(
3860
3915
  "click",
3861
3916
  (event) => {
3862
- if (this.options.active()) event.stopImmediatePropagation();
3917
+ if (this.options.tool()) event.stopImmediatePropagation();
3863
3918
  },
3864
3919
  { ...options, capture: true }
3865
3920
  );
@@ -3867,6 +3922,13 @@ var RectangleDrawTool = class {
3867
3922
  viewport.addEventListener("pointermove", (event) => this.#move(event), options);
3868
3923
  viewport.addEventListener("pointerup", (event) => void this.#finish(event), options);
3869
3924
  viewport.addEventListener("pointercancel", () => this.#cancel(), options);
3925
+ viewport.addEventListener(
3926
+ "lostpointercapture",
3927
+ (event) => {
3928
+ if (event.pointerId === this.#drag?.pointer) this.#cancel();
3929
+ },
3930
+ options
3931
+ );
3870
3932
  viewport.addEventListener(
3871
3933
  "keydown",
3872
3934
  (event) => {
@@ -3884,61 +3946,87 @@ var RectangleDrawTool = class {
3884
3946
  };
3885
3947
  }
3886
3948
  #start(event) {
3887
- if (!this.options.active() || event.button !== 0 || this.#drag) return;
3949
+ const kind = this.options.tool();
3950
+ if (!kind || event.button !== 0 || this.#drag) return;
3888
3951
  const state = this.controller.state;
3889
3952
  const page2 = state.document?.pages[state.pageIndex];
3890
3953
  const svg = this.viewport.querySelector("svg.paper");
3891
3954
  if (!page2 || !svg || !state.edit.sourceAvailable || state.edit.busy) return;
3892
- const start = pagePoint(svg, page2, event);
3955
+ const start = pagePoint(svg, page2, event, { bounded: false });
3893
3956
  if (!start) return;
3894
3957
  event.preventDefault();
3895
- const rect = this.viewport.ownerDocument.createElementNS("http://www.w3.org/2000/svg", "rect");
3958
+ const rect = this.viewport.ownerDocument.createElementNS(
3959
+ "http://www.w3.org/2000/svg",
3960
+ kind === "line" ? "line" : kind === "ellipse" ? "ellipse" : "rect"
3961
+ );
3896
3962
  rect.classList.add("draw-preview");
3897
3963
  rect.setAttribute("vector-effect", "non-scaling-stroke");
3898
3964
  svg.append(rect);
3899
- this.#drag = { pointer: event.pointerId, svg, page: page2, ...start, rect };
3965
+ this.#drag = { pointer: event.pointerId, svg, page: page2, ...start, rect, kind };
3900
3966
  this.viewport.setPointerCapture?.(event.pointerId);
3901
3967
  this.#update(start);
3902
3968
  }
3903
3969
  #move(event) {
3904
3970
  if (!this.#drag || event.pointerId !== this.#drag.pointer) return;
3905
- const point = pagePoint(this.#drag.svg, this.#drag.page, event);
3971
+ const point = pagePoint(this.#drag.svg, this.#drag.page, event, {
3972
+ bounded: false
3973
+ });
3906
3974
  if (point) this.#update(point);
3907
3975
  }
3908
3976
  #update(point) {
3909
- const { x, y, rect } = this.#drag;
3977
+ const { x, y, rect, kind } = this.#drag;
3978
+ if (kind === "line") {
3979
+ for (const [name, value] of Object.entries({ x1: x, y1: y, x2: point.x, y2: point.y }))
3980
+ rect.setAttribute(name, String(value));
3981
+ return;
3982
+ }
3983
+ if (kind === "ellipse") {
3984
+ for (const [name, value] of Object.entries({
3985
+ cx: (x + point.x) / 2,
3986
+ cy: (y + point.y) / 2,
3987
+ rx: Math.abs(point.x - x) / 2,
3988
+ ry: Math.abs(point.y - y) / 2
3989
+ }))
3990
+ rect.setAttribute(name, String(value));
3991
+ return;
3992
+ }
3910
3993
  rect.setAttribute("x", String(Math.min(x, point.x)));
3911
3994
  rect.setAttribute("y", String(Math.min(y, point.y)));
3912
3995
  rect.setAttribute("width", String(Math.abs(point.x - x)));
3913
3996
  rect.setAttribute("height", String(Math.abs(point.y - y)));
3914
3997
  }
3915
3998
  #cancel() {
3916
- this.#drag?.rect.remove();
3999
+ const drag = this.#drag;
4000
+ drag?.rect.remove();
3917
4001
  this.#drag = void 0;
4002
+ if (drag && this.viewport.hasPointerCapture?.(drag.pointer))
4003
+ this.viewport.releasePointerCapture(drag.pointer);
3918
4004
  }
3919
4005
  async #finish(event) {
3920
4006
  const drag = this.#drag;
3921
4007
  if (!drag || event.pointerId !== drag.pointer) return;
3922
- const end = pagePoint(drag.svg, drag.page, event) ?? drag;
4008
+ const end = pagePoint(drag.svg, drag.page, event, { bounded: false }) ?? drag;
3923
4009
  this.#cancel();
3924
4010
  const width = Math.abs(end.x - drag.x), height = Math.abs(end.y - drag.y);
3925
- if (width < MIN_SIZE || height < MIN_SIZE) {
3926
- this.options.announce("Drag on the page to draw a rectangle.");
4011
+ if (drag.kind === "line" ? Math.hypot(width, height) < MIN_SIZE : width < MIN_SIZE || height < MIN_SIZE) {
4012
+ this.options.announce(`Drag on the page to draw a ${drag.kind}.`);
3927
4013
  return;
3928
4014
  }
3929
4015
  const state = this.controller.state;
3930
- if (state.document?.pages[state.pageIndex] !== drag.page) return;
4016
+ if (state.document?.pages[state.pageIndex] !== drag.page || this.options.tool() !== drag.kind)
4017
+ return;
3931
4018
  const request = ++this.#request;
3932
4019
  try {
3933
- const shapeId = await insertRectangle(
4020
+ const shapeId = drag.kind === "line" ? await insertLine(this.controller, drag.page, drag, end) : await insertBox(
3934
4021
  this.controller,
3935
4022
  drag.page,
3936
4023
  { x: (drag.x + end.x) / 2, y: (drag.y + end.y) / 2 },
3937
- { width, height }
4024
+ { width, height },
4025
+ drag.kind
3938
4026
  );
3939
4027
  if (request !== this.#request) return;
3940
4028
  this.options.announce(
3941
- `Rectangle ${shapeId} added (${+width.toFixed(4)} \xD7 ${+height.toFixed(4)} in).`
4029
+ drag.kind === "line" ? `Line ${shapeId} added.` : `${drag.kind === "ellipse" ? "Ellipse" : "Rectangle"} ${shapeId} added (${+width.toFixed(4)} \xD7 ${+height.toFixed(4)} in).`
3942
4030
  );
3943
4031
  } catch (error) {
3944
4032
  if (request === this.#request && !isEditCancellation(error))
@@ -4092,13 +4180,29 @@ var ViewerGeometryControls = class {
4092
4180
  this.controller = controller;
4093
4181
  this.draftChanged = draftChanged;
4094
4182
  const field = (name) => panel.querySelector(`[data-geometry-field="${name}"]`);
4183
+ const doc = panel.ownerDocument;
4184
+ const label = doc.createElement("label");
4185
+ label.textContent = "Angle (degrees) ";
4186
+ const angle = doc.createElement("input");
4187
+ angle.type = "number";
4188
+ angle.step = "any";
4189
+ angle.dataset.geometryField = "angle";
4190
+ label.append(angle);
4191
+ const actions = panel.querySelector("[data-geometry] .edit-actions");
4192
+ actions.before(label);
4193
+ const rotate = doc.createElement("button");
4194
+ rotate.type = "button";
4195
+ rotate.dataset.geometryAction = "rotate-shape";
4196
+ rotate.textContent = "Rotate selected";
4197
+ actions.append(rotate);
4095
4198
  this.#fields = {
4096
4199
  id: field("id"),
4097
4200
  text: field("text"),
4098
4201
  x: field("x"),
4099
4202
  y: field("y"),
4100
4203
  width: field("width"),
4101
- height: field("height")
4204
+ height: field("height"),
4205
+ angle
4102
4206
  };
4103
4207
  this.#buttons = [...panel.querySelectorAll("[data-geometry-action]")];
4104
4208
  this.#error = panel.querySelector("[data-geometry-error]");
@@ -4157,7 +4261,7 @@ var ViewerGeometryControls = class {
4157
4261
  width,
4158
4262
  height,
4159
4263
  ...this.#fields.text.value ? { text: this.#fields.text.value } : {}
4160
- } : type === "move-shape" ? { type, ...target, x, y } : type === "resize-shape" ? { type, ...target, width, height } : { type: "delete-shape", ...target };
4264
+ } : type === "move-shape" ? { type, ...target, x, y } : type === "resize-shape" ? { type, ...target, width, height } : type === "rotate-shape" ? { type, ...target, angle: this.#fields.angle.valueAsNumber * Math.PI / 180 } : { type: "delete-shape", ...target };
4161
4265
  const request = ++this.#request;
4162
4266
  this.#error.hidden = true;
4163
4267
  try {
@@ -4204,9 +4308,11 @@ var ViewerGeometryControls = class {
4204
4308
  const size = [this.#fields.width, this.#fields.height].every(
4205
4309
  (field) => Number.isFinite(field.valueAsNumber) && field.valueAsNumber > 0
4206
4310
  );
4311
+ const selected = selectedShape(state.document, state.selectedShape, state.pageIndex);
4312
+ const lineSize = selected?.kind === "connector" && selected.height === 0 && Number.isFinite(this.#fields.width.valueAsNumber) && this.#fields.width.valueAsNumber > 0 && this.#fields.height.valueAsNumber === 0;
4207
4313
  for (const button of this.#buttons) {
4208
4314
  const action2 = button.dataset.geometryAction;
4209
- button.disabled = !available || (action2 === "create-rectangle" ? !this.#fields.id.value || !position || !size : !state.selectedShape || action2 === "move-shape" && !position || action2 === "resize-shape" && !size);
4315
+ button.disabled = !available || (action2 === "create-rectangle" ? !this.#fields.id.value || !position || !size : !state.selectedShape || action2 === "move-shape" && !position || action2 === "resize-shape" && !size && !lineSize || action2 === "rotate-shape" && (!Number.isFinite(this.#fields.angle.valueAsNumber) || selected?.kind === "connector"));
4210
4316
  }
4211
4317
  }
4212
4318
  };
@@ -4672,6 +4778,17 @@ function fillPaint(style, defs, resources, world, pageHeight) {
4672
4778
  }
4673
4779
  return gradientPaint(paint, defs);
4674
4780
  }
4781
+ function linePaint(style, defs, resources, world, pageHeight, size) {
4782
+ const gradient = visioStrokeGradient(style.lineGradient, style.lineWidth, size);
4783
+ if (!gradient) return safeColor(style.lineColor);
4784
+ return fillPaint(
4785
+ { fill: style.lineColor, fillGradient: gradient },
4786
+ defs,
4787
+ resources,
4788
+ world,
4789
+ pageHeight
4790
+ );
4791
+ }
4675
4792
  function gradientPaint(paint, defs, normalized = false, rotation) {
4676
4793
  const gradient = svgElement(paint.type === "radial" ? "radialGradient" : "linearGradient");
4677
4794
  gradient.id = `visio-fill-${++gradientId}`;
@@ -5436,7 +5553,7 @@ function renderPage(model, page2, options = {}) {
5436
5553
  vectorBudget: new ForeignVectorBudget(),
5437
5554
  renderable: /* @__PURE__ */ new WeakMap(),
5438
5555
  visible,
5439
- interactive: !options.static
5556
+ interactive: !options.static && options.interactive !== false
5440
5557
  };
5441
5558
  try {
5442
5559
  for (const layer of pages)
@@ -5445,7 +5562,7 @@ function renderPage(model, page2, options = {}) {
5445
5562
  context.resources.dispose();
5446
5563
  throw error;
5447
5564
  }
5448
- if (!options.static) {
5565
+ if (context.interactive) {
5449
5566
  const first = svg.querySelector("[data-shape-id]");
5450
5567
  first?.setAttribute("tabindex", "0");
5451
5568
  }
@@ -5493,6 +5610,10 @@ function drawShape(shape, parent, context, pageId, parentTransform = VISIO_IDENT
5493
5610
  function drawOwn(shape, group2, context, world) {
5494
5611
  const { warnings, defs, resources } = context;
5495
5612
  const fill = shape.geometry.some((geometry) => geometry.fill) ? fillPaint(shape.style, defs, resources, world, context.pageHeight) : "none";
5613
+ const stroke = shape.geometry.some((geometry) => geometry.stroke) && shape.style.linePattern !== 0 ? linePaint(shape.style, defs, resources, world, context.pageHeight, [
5614
+ shape.width,
5615
+ shape.height
5616
+ ]) : "none";
5496
5617
  for (const geometry of shape.geometry) {
5497
5618
  if (++context.nodes > 5e4) {
5498
5619
  warnings.add(
@@ -5507,7 +5628,7 @@ function drawOwn(shape, group2, context, world) {
5507
5628
  path.setAttribute("fill", geometry.fill ? fill : "none");
5508
5629
  path.setAttribute("fill-opacity", String(shape.style.fillOpacity));
5509
5630
  const stroked = geometry.stroke && shape.style.linePattern !== 0;
5510
- path.setAttribute("stroke", stroked ? safeColor(shape.style.lineColor) : "none");
5631
+ path.setAttribute("stroke", stroked ? stroke : "none");
5511
5632
  path.setAttribute("stroke-width", String(shape.style.lineWidth));
5512
5633
  path.setAttribute("stroke-opacity", String(shape.style.lineOpacity));
5513
5634
  path.setAttribute("stroke-linejoin", "round");
@@ -5656,6 +5777,10 @@ function routeRibbonAction(targets, action2) {
5656
5777
  return targets.history(action2.key);
5657
5778
  case "delete":
5658
5779
  return targets.deleteSelection();
5780
+ case "rotate":
5781
+ return targets.rotateSelection(action2.direction);
5782
+ case "flip":
5783
+ return targets.flipSelection(action2.axis);
5659
5784
  case "tool":
5660
5785
  return targets.setTool(action2.tool);
5661
5786
  case "grid":
@@ -5946,14 +6071,16 @@ var ViewerCommands = class {
5946
6071
  this.#pageOrder = new ViewerPageOrder(host.root, host.controller);
5947
6072
  this.#pageRename = new ViewerPageRename(host.root, host.controller);
5948
6073
  this.#pageDelete = new ViewerPageDelete(host.root, host.controller);
5949
- this.#draw = new RectangleDrawTool(host.viewport, host.controller, {
5950
- active: () => this.#tool === "rectangle",
6074
+ this.#draw = new ShapeDrawTool(host.viewport, host.controller, {
6075
+ tool: () => this.#tool === "pointer" ? void 0 : this.#tool,
5951
6076
  announce: host.announce
5952
6077
  });
5953
6078
  this.#targets = {
5954
6079
  controller: host.controller,
5955
6080
  history: (key) => this.#history(key),
5956
6081
  deleteSelection: () => this.#delete(),
6082
+ rotateSelection: (direction) => this.#transform({ type: "rotate", direction }),
6083
+ flipSelection: (axis) => this.#transform({ type: "flip", axis }),
5957
6084
  setTool: (tool) => this.setTool(tool),
5958
6085
  toggleGrid: () => {
5959
6086
  this.#grid = !this.#grid;
@@ -6058,9 +6185,10 @@ var ViewerCommands = class {
6058
6185
  };
6059
6186
  }
6060
6187
  setTool(tool) {
6061
- if (tool === "rectangle" && !this.#canEdit(this.host.controller.state)) return;
6188
+ if (tool !== "pointer" && !this.#canEdit(this.host.controller.state)) return;
6062
6189
  this.#tool = tool;
6063
6190
  this.render(this.host.controller.state);
6191
+ this.host.toolChanged?.();
6064
6192
  }
6065
6193
  #canEdit(state) {
6066
6194
  return state.edit.sourceAvailable && !state.loading && !state.edit.busy;
@@ -6094,15 +6222,35 @@ var ViewerCommands = class {
6094
6222
  if (index >= 0) this.host.controller.setPage(index);
6095
6223
  }, "Inserted a blank page.");
6096
6224
  }
6225
+ #transform(action2) {
6226
+ const state = this.host.controller.state;
6227
+ const page2 = state.document?.pages[state.pageIndex];
6228
+ if (!page2 || !state.selectedShape || !this.#canEdit(state)) return;
6229
+ const command2 = action2.type === "rotate" ? visioQuarterTurnCommand(page2, state.selectedShape.id, action2.direction) : visioLocalRotationShape(page2, state.selectedShape.id, false) ? {
6230
+ type: "flip-shape",
6231
+ pageId: page2.id,
6232
+ shapeId: state.selectedShape.id,
6233
+ axis: action2.axis
6234
+ } : void 0;
6235
+ if (command2)
6236
+ void this.#edit(
6237
+ () => this.host.controller.applyEdits([command2]),
6238
+ action2.type === "rotate" ? `Rotated ${action2.direction} 90\xB0.` : `Flipped ${action2.axis}.`
6239
+ );
6240
+ }
6097
6241
  async #edit(action2, success) {
6098
6242
  const request = ++this.#pending;
6099
6243
  const { root, viewport } = this.host;
6100
6244
  const canvasFocused = viewport.contains(root.activeElement);
6245
+ const generation = this.host.controller.documentGeneration;
6101
6246
  try {
6102
6247
  await action2();
6103
6248
  if (canvasFocused && !viewport.contains(root.activeElement))
6104
6249
  viewport.focus({ preventScroll: true });
6105
- if (success && request === this.#pending) this.host.announce(success);
6250
+ if (success && request === this.#pending)
6251
+ this.host.announce(
6252
+ this.host.controller.documentGeneration === generation ? "No changes were made." : success
6253
+ );
6106
6254
  } catch (error) {
6107
6255
  if (request === this.#pending && !isEditCancellation(error) && !this.host.controller.state.edit.error)
6108
6256
  this.host.announce(editErrorMessage(error));
@@ -6132,6 +6280,8 @@ var ViewerCommands = class {
6132
6280
  return { type: "history", key: "redo" };
6133
6281
  if (control && key === "1") return { type: "tool", tool: "pointer" };
6134
6282
  if (control && key === "8") return { type: "tool", tool: "rectangle" };
6283
+ if (control && key === "9") return { type: "tool", tool: "ellipse" };
6284
+ if (control && key === "6") return { type: "tool", tool: "line" };
6135
6285
  if (!control && key === "Delete" && state.selectedShape) return { type: "delete" };
6136
6286
  if (!control && key === "F2" && state.document)
6137
6287
  return { type: "reveal", panel: "edit", focusText: true };
@@ -6154,8 +6304,14 @@ var ViewerCommands = class {
6154
6304
  const button = (name) => root.querySelector(`[command="${name}"]`);
6155
6305
  const box = (name) => root.querySelector(`[data-check="${name}"]`);
6156
6306
  const editing = this.#canEdit(state);
6157
- if (this.#tool === "rectangle" && !state.edit.sourceAvailable) this.#tool = "pointer";
6307
+ if (this.#tool !== "pointer" && !state.edit.sourceAvailable) this.#tool = "pointer";
6158
6308
  const page2 = state.document?.pages[state.pageIndex];
6309
+ const rotating = editing && !!page2 && !!state.selectedShape && !!visioLocalRotationShape(page2, state.selectedShape.id);
6310
+ for (const name of ["rotate-left", "rotate-right"]) button(name).disabled = !rotating;
6311
+ const flipping = rotating && !!page2 && !!state.selectedShape && !!visioLocalRotationShape(page2, state.selectedShape.id, false);
6312
+ for (const name of ["flip-horizontal", "flip-vertical"]) button(name).disabled = !flipping;
6313
+ root.querySelector('[data-menu="rotate"]').disabled = !rotating;
6314
+ root.querySelector('[data-menu="position"]').disabled = !rotating;
6159
6315
  button("undo").disabled = !state.edit.canUndo || state.edit.busy || state.loading;
6160
6316
  button("redo").disabled = !state.edit.canRedo || state.edit.busy || state.loading;
6161
6317
  button("pointer").setAttribute("pressed", String(this.#tool === "pointer"));
@@ -6164,6 +6320,10 @@ var ViewerCommands = class {
6164
6320
  rectangle.disabled = !editing || !page2;
6165
6321
  button("rectangle-item").setAttribute("checked", String(this.#tool === "rectangle"));
6166
6322
  button("rectangle-item").disabled = !editing || !page2;
6323
+ button("line-tool").setAttribute("checked", String(this.#tool === "line"));
6324
+ button("ellipse").setAttribute("checked", String(this.#tool === "ellipse"));
6325
+ button("ellipse").disabled = !editing || !page2;
6326
+ button("line-tool").disabled = !editing || !page2;
6167
6327
  rectangle.title = state.edit.sourceAvailable ? "Rectangle (Ctrl+8)" : "Rectangle (Ctrl+8): open a .vsdx file to draw. Model-only documents are read only.";
6168
6328
  box("grid").checked = this.#grid;
6169
6329
  box("grid").disabled = !page2;
@@ -6181,6 +6341,464 @@ var ViewerCommands = class {
6181
6341
  }
6182
6342
  };
6183
6343
 
6344
+ // src/visio/viewer-handle-events.ts
6345
+ function handleGestureIsCurrent(drag, state, active) {
6346
+ return state.document === drag.document && state.document?.pages[state.pageIndex] === drag.page && state.selectedShape?.id === drag.shapeId && drag.svg.isConnected && !state.loading && !state.edit.busy && active;
6347
+ }
6348
+ function wireHandleEvents(viewport, callbacks) {
6349
+ const Abort = viewport.ownerDocument.defaultView?.AbortController ?? AbortController;
6350
+ const events = new Abort(), options = { signal: events.signal, capture: true };
6351
+ let suppressClick = false;
6352
+ viewport.addEventListener(
6353
+ "pointerdown",
6354
+ (event) => {
6355
+ suppressClick = false;
6356
+ callbacks.start(event);
6357
+ },
6358
+ options
6359
+ );
6360
+ viewport.addEventListener("pointermove", (event) => callbacks.move(event), options);
6361
+ viewport.addEventListener(
6362
+ "pointerup",
6363
+ (event) => {
6364
+ if (event.pointerId === callbacks.pointer()) suppressClick = true;
6365
+ void callbacks.finish(event);
6366
+ },
6367
+ options
6368
+ );
6369
+ const cancel = (event) => {
6370
+ if (event.pointerId === callbacks.pointer()) callbacks.cancel();
6371
+ };
6372
+ viewport.addEventListener("pointercancel", cancel, options);
6373
+ viewport.addEventListener("lostpointercapture", cancel, options);
6374
+ viewport.addEventListener(
6375
+ "keydown",
6376
+ (event) => {
6377
+ if (event.key === "Escape" && callbacks.pointer() !== void 0) {
6378
+ event.preventDefault();
6379
+ event.stopImmediatePropagation();
6380
+ suppressClick = true;
6381
+ callbacks.cancel();
6382
+ }
6383
+ },
6384
+ options
6385
+ );
6386
+ viewport.addEventListener(
6387
+ "click",
6388
+ (event) => {
6389
+ if (suppressClick) {
6390
+ suppressClick = false;
6391
+ event.stopImmediatePropagation();
6392
+ }
6393
+ },
6394
+ options
6395
+ );
6396
+ return () => {
6397
+ callbacks.cancel();
6398
+ events.abort();
6399
+ };
6400
+ }
6401
+
6402
+ // src/visio/viewer-line-endpoints.ts
6403
+ var SVG = "http://www.w3.org/2000/svg";
6404
+ var pointOptions = { snap: false, bounded: false };
6405
+ var ViewerLineEndpoints = class {
6406
+ constructor(viewport, controller, options) {
6407
+ this.viewport = viewport;
6408
+ this.controller = controller;
6409
+ this.options = options;
6410
+ }
6411
+ viewport;
6412
+ controller;
6413
+ options;
6414
+ #drag;
6415
+ #request = 0;
6416
+ #overlay;
6417
+ render(state) {
6418
+ const page2 = state.document?.pages[state.pageIndex];
6419
+ if (this.#drag) {
6420
+ if (handleGestureIsCurrent(this.#drag, state, this.options.active())) return;
6421
+ this.#cancel();
6422
+ }
6423
+ this.#overlay?.remove();
6424
+ this.#overlay = void 0;
6425
+ if (!page2 || !state.selectedShape || !state.edit.sourceAvailable || state.edit.busy || state.loading || !this.options.active())
6426
+ return;
6427
+ const shape = page2.shapes.find((shape2) => shape2.id === state.selectedShape.id);
6428
+ if (!shape || !visioStraightLineHandles(shape) || page2.connectors.some(
6429
+ (connection) => connection.fromShapeId === shape.id || connection.toShapeId === shape.id
6430
+ ))
6431
+ return;
6432
+ const group2 = Array.from(this.viewport.querySelectorAll("[data-shape-id]")).find(
6433
+ (group3) => group3.dataset.shapeId === shape.id && group3.dataset.selected === "true"
6434
+ );
6435
+ if (!group2) return;
6436
+ const svg = group2.ownerSVGElement;
6437
+ if (!svg) return;
6438
+ const overlay = this.viewport.ownerDocument.createElementNS(SVG, "g");
6439
+ overlay.dataset.lineEndpointOverlay = "";
6440
+ const rootMatrix = svg.getCTM?.(), groupMatrix = group2.getCTM?.();
6441
+ if (rootMatrix && groupMatrix) {
6442
+ const matrix2 = rootMatrix.inverse().multiply(groupMatrix);
6443
+ overlay.setAttribute(
6444
+ "transform",
6445
+ `matrix(${matrix2.a} ${matrix2.b} ${matrix2.c} ${matrix2.d} ${matrix2.e} ${matrix2.f})`
6446
+ );
6447
+ }
6448
+ const screen = group2.getScreenCTM?.();
6449
+ const screenScale = screen ? Math.hypot(screen.a, screen.b) : 96 * state.zoom;
6450
+ for (const endpoint of ["begin", "end"]) {
6451
+ const handle = this.viewport.ownerDocument.createElementNS(SVG, "circle");
6452
+ handle.dataset.lineEndpoint = endpoint;
6453
+ handle.dataset.lineShapeId = shape.id;
6454
+ handle.setAttribute("cx", String(endpoint === "begin" ? 0 : shape.width));
6455
+ handle.setAttribute("cy", "0");
6456
+ handle.setAttribute("r", String(5 / screenScale));
6457
+ handle.setAttribute("vector-effect", "non-scaling-stroke");
6458
+ handle.setAttribute("aria-label", `Drag ${endpoint} endpoint`);
6459
+ overlay.append(handle);
6460
+ }
6461
+ svg.append(overlay);
6462
+ this.#overlay = overlay;
6463
+ }
6464
+ wire() {
6465
+ const dispose = wireHandleEvents(this.viewport, {
6466
+ pointer: () => this.#drag?.pointer,
6467
+ start: (event) => this.#start(event),
6468
+ move: (event) => this.#move(event),
6469
+ finish: (event) => this.#finish(event),
6470
+ cancel: () => this.#cancel()
6471
+ });
6472
+ return () => {
6473
+ ++this.#request;
6474
+ dispose();
6475
+ this.#overlay?.remove();
6476
+ this.#overlay = void 0;
6477
+ };
6478
+ }
6479
+ #start(event) {
6480
+ const handle = event.target?.closest?.("[data-line-endpoint]");
6481
+ const state = this.controller.state, page2 = state.document?.pages[state.pageIndex];
6482
+ const svg = this.viewport.querySelector("svg.paper");
6483
+ const endpoint = handle?.dataset.lineEndpoint;
6484
+ if (!handle || !svg || !page2 || !state.document || !state.selectedShape || this.#drag || event.button !== 0 || state.edit.busy || state.loading || !state.edit.sourceAvailable || !this.options.active() || endpoint !== "begin" && endpoint !== "end")
6485
+ return;
6486
+ const other = handle.parentElement?.querySelector(
6487
+ `[data-line-endpoint="${endpoint === "begin" ? "end" : "begin"}"]`
6488
+ );
6489
+ const matrix2 = other?.getScreenCTM();
6490
+ if (!other || !matrix2) return;
6491
+ const centre = new DOMPoint(other.cx.baseVal.value, 0).matrixTransform(matrix2);
6492
+ const fixed = pagePoint(svg, page2, { clientX: centre.x, clientY: centre.y }, pointOptions);
6493
+ const start = pagePoint(svg, page2, event, pointOptions);
6494
+ if (!fixed || !start) return;
6495
+ event.preventDefault();
6496
+ event.stopImmediatePropagation();
6497
+ const preview2 = this.viewport.ownerDocument.createElementNS(SVG, "line");
6498
+ preview2.classList.add("endpoint-preview");
6499
+ preview2.setAttribute("vector-effect", "non-scaling-stroke");
6500
+ for (const [name, value] of Object.entries({
6501
+ x1: fixed.x,
6502
+ y1: fixed.y,
6503
+ x2: start.x,
6504
+ y2: start.y
6505
+ }))
6506
+ preview2.setAttribute(name, String(value));
6507
+ svg.append(preview2);
6508
+ this.#drag = {
6509
+ pointer: event.pointerId,
6510
+ svg,
6511
+ page: page2,
6512
+ document: state.document,
6513
+ shapeId: state.selectedShape.id,
6514
+ endpoint,
6515
+ preview: preview2,
6516
+ startX: event.clientX,
6517
+ startY: event.clientY
6518
+ };
6519
+ this.viewport.setPointerCapture?.(event.pointerId);
6520
+ }
6521
+ #move(event) {
6522
+ const drag = this.#drag;
6523
+ if (!drag || event.pointerId !== drag.pointer) return;
6524
+ const point = pagePoint(drag.svg, drag.page, event, pointOptions);
6525
+ if (point) {
6526
+ drag.preview.setAttribute("x2", String(point.x));
6527
+ drag.preview.setAttribute("y2", String(point.y));
6528
+ }
6529
+ event.preventDefault();
6530
+ event.stopImmediatePropagation();
6531
+ }
6532
+ #cancel() {
6533
+ const drag = this.#drag;
6534
+ this.#drag = void 0;
6535
+ drag?.preview.remove();
6536
+ if (drag && this.viewport.hasPointerCapture?.(drag.pointer))
6537
+ this.viewport.releasePointerCapture(drag.pointer);
6538
+ }
6539
+ async #finish(event) {
6540
+ const drag = this.#drag;
6541
+ if (!drag || event.pointerId !== drag.pointer) return;
6542
+ event.preventDefault();
6543
+ event.stopImmediatePropagation();
6544
+ const point = pagePoint(drag.svg, drag.page, event, pointOptions);
6545
+ this.#cancel();
6546
+ if (!point || Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) < 0.5 || this.controller.state.document !== drag.document)
6547
+ return;
6548
+ const request = ++this.#request;
6549
+ try {
6550
+ await this.controller.applyEdits([
6551
+ visioPageEditToDrawing(drag.page, {
6552
+ type: "move-line-endpoint",
6553
+ pageId: drag.page.id,
6554
+ shapeId: drag.shapeId,
6555
+ endpoint: drag.endpoint,
6556
+ x: point.x,
6557
+ y: drag.page.height - point.y
6558
+ })
6559
+ ]);
6560
+ if (request === this.#request)
6561
+ this.options.announce(`${drag.endpoint === "begin" ? "Begin" : "End"} endpoint moved.`);
6562
+ } catch (error) {
6563
+ if (request === this.#request && !isEditCancellation(error))
6564
+ this.options.announce(editErrorMessage(error));
6565
+ }
6566
+ }
6567
+ };
6568
+ function createRotationPreview(model, page2, shape, source) {
6569
+ const element = svgElement("g");
6570
+ element.classList.add("rotation-shape-preview");
6571
+ element.setAttribute("aria-hidden", "true");
6572
+ element.style.pointerEvents = "none";
6573
+ const visibility = source.style.visibility;
6574
+ let result;
6575
+ source.after(element);
6576
+ source.style.visibility = "hidden";
6577
+ return {
6578
+ element,
6579
+ update(angle) {
6580
+ const transform = visioRotationPreviewTransform(shape, angle);
6581
+ if (!transform) return;
6582
+ const previewPage = {
6583
+ ...page2,
6584
+ shapes: [{ ...shape, transform, rotation: { ...shape.rotation, angle } }]
6585
+ };
6586
+ delete previewPage.backgroundPageId;
6587
+ const next = renderPage({ ...model, pages: [previewPage] }, previewPage, {
6588
+ interactive: false
6589
+ });
6590
+ const defs = next.svg.querySelector(":scope > defs");
6591
+ const root = next.svg.querySelector(":scope > g");
6592
+ element.replaceChildren(defs, ...root.children);
6593
+ result?.dispose();
6594
+ result = next;
6595
+ },
6596
+ dispose() {
6597
+ element.remove();
6598
+ result?.dispose();
6599
+ result = void 0;
6600
+ source.style.visibility = visibility;
6601
+ }
6602
+ };
6603
+ }
6604
+
6605
+ // src/visio/viewer-rotation-handle.ts
6606
+ var SVG2 = "http://www.w3.org/2000/svg";
6607
+ var pointOptions2 = { snap: false, bounded: false };
6608
+ var ViewerRotationHandle = class {
6609
+ constructor(viewport, controller, options) {
6610
+ this.viewport = viewport;
6611
+ this.controller = controller;
6612
+ this.options = options;
6613
+ }
6614
+ viewport;
6615
+ controller;
6616
+ options;
6617
+ #drag;
6618
+ #request = 0;
6619
+ #overlay;
6620
+ render(state) {
6621
+ if (this.#drag) {
6622
+ if (handleGestureIsCurrent(this.#drag, state, this.options.active())) return;
6623
+ this.#cancel();
6624
+ }
6625
+ this.#overlay?.remove();
6626
+ this.#overlay = void 0;
6627
+ const page2 = state.document?.pages[state.pageIndex], selection2 = state.selectedShape;
6628
+ if (!page2 || !selection2 || !state.edit.sourceAvailable || state.loading || state.edit.busy || !this.options.active())
6629
+ return;
6630
+ const shape = visioLocalRotationShape(page2, selection2.id);
6631
+ if (!shape?.rotation) return;
6632
+ const group2 = Array.from(this.viewport.querySelectorAll("[data-shape-id]")).find(
6633
+ (group3) => group3.dataset.shapeId === shape.id && group3.dataset.selected === "true"
6634
+ );
6635
+ const svg = group2?.ownerSVGElement, matrix2 = svg?.getScreenCTM?.();
6636
+ if (!group2 || !svg || !matrix2) return;
6637
+ const center = { x: shape.rotation.pinX, y: page2.height - shape.rotation.pinY };
6638
+ const rect = group2.getBoundingClientRect(), bounds = this.viewport.getBoundingClientRect();
6639
+ const location = resolveRotateHandlePlacement({
6640
+ preferred: { x: (rect.left + rect.right) / 2, y: rect.top - 24 },
6641
+ selection: rect,
6642
+ bounds,
6643
+ hitWidth: 14,
6644
+ hitHeight: 14,
6645
+ obstacles: []
6646
+ });
6647
+ if (!location) return;
6648
+ const point = { x: location.x - bounds.left, y: location.y - bounds.top };
6649
+ const pivot = new DOMPoint(center.x, center.y).matrixTransform(matrix2);
6650
+ const overlay = this.viewport.ownerDocument.createElementNS(SVG2, "svg");
6651
+ overlay.classList.add("rotation-overlay");
6652
+ overlay.style.left = `${this.viewport.scrollLeft}px`;
6653
+ overlay.style.top = `${this.viewport.scrollTop}px`;
6654
+ overlay.setAttribute("width", String(bounds.width));
6655
+ overlay.setAttribute("height", String(bounds.height));
6656
+ overlay.setAttribute("viewBox", `0 0 ${bounds.width} ${bounds.height}`);
6657
+ overlay.dataset.rotationOverlay = "";
6658
+ const stem = this.viewport.ownerDocument.createElementNS(SVG2, "line");
6659
+ for (const [name, value] of Object.entries({
6660
+ x1: pivot.x - bounds.left,
6661
+ y1: pivot.y - bounds.top,
6662
+ x2: point.x,
6663
+ y2: point.y
6664
+ }))
6665
+ stem.setAttribute(name, String(value));
6666
+ stem.classList.add("rotation-stem");
6667
+ stem.setAttribute("vector-effect", "non-scaling-stroke");
6668
+ const handle = this.viewport.ownerDocument.createElementNS(SVG2, "circle");
6669
+ handle.dataset.rotationHandle = shape.id;
6670
+ handle.setAttribute("cx", String(point.x));
6671
+ handle.setAttribute("cy", String(point.y));
6672
+ handle.setAttribute("r", "6");
6673
+ handle.setAttribute("vector-effect", "non-scaling-stroke");
6674
+ handle.setAttribute("aria-label", "Drag rotation handle");
6675
+ overlay.append(stem, handle);
6676
+ this.viewport.append(overlay);
6677
+ this.#overlay = overlay;
6678
+ }
6679
+ wire() {
6680
+ const dispose = wireHandleEvents(this.viewport, {
6681
+ pointer: () => this.#drag?.pointer,
6682
+ start: (event) => this.#start(event),
6683
+ move: (event) => this.#move(event),
6684
+ finish: (event) => this.#finish(event),
6685
+ cancel: () => this.#cancel()
6686
+ });
6687
+ const refresh = () => this.render(this.controller.state);
6688
+ this.viewport.addEventListener("scroll", refresh, { passive: true });
6689
+ const Observer = this.viewport.ownerDocument.defaultView?.ResizeObserver;
6690
+ const observer = Observer ? new Observer(refresh) : void 0;
6691
+ observer?.observe(this.viewport);
6692
+ return () => {
6693
+ this.viewport.removeEventListener("scroll", refresh);
6694
+ observer?.disconnect();
6695
+ ++this.#request;
6696
+ dispose();
6697
+ this.#overlay?.remove();
6698
+ this.#overlay = void 0;
6699
+ };
6700
+ }
6701
+ #start(event) {
6702
+ const handle = event.target?.closest?.("[data-rotation-handle]");
6703
+ const state = this.controller.state, page2 = state.document?.pages[state.pageIndex];
6704
+ const shape = page2?.shapes.find((shape2) => shape2.id === handle?.dataset.rotationHandle);
6705
+ const svg = this.viewport.querySelector("svg.paper");
6706
+ if (!handle || !svg || !page2 || !state.document || !shape?.rotation || state.selectedShape?.id !== shape.id || this.#drag || event.button !== 0 || state.edit.busy || state.loading || !state.edit.sourceAvailable || !this.options.active())
6707
+ return;
6708
+ const source = Array.from(svg.querySelectorAll("[data-shape-id]")).find(
6709
+ (group2) => group2.dataset.shapeId === shape.id
6710
+ );
6711
+ if (!source) return;
6712
+ const start = pagePoint(svg, page2, event, pointOptions2);
6713
+ if (!start) return;
6714
+ this.viewport.focus({ preventScroll: true });
6715
+ const center = { x: shape.rotation.pinX, y: page2.height - shape.rotation.pinY };
6716
+ const preview2 = this.viewport.ownerDocument.createElementNS(SVG2, "line");
6717
+ preview2.classList.add("rotation-preview");
6718
+ preview2.setAttribute("vector-effect", "non-scaling-stroke");
6719
+ for (const [name, value] of Object.entries({
6720
+ x1: center.x,
6721
+ y1: center.y,
6722
+ x2: start.x,
6723
+ y2: start.y
6724
+ }))
6725
+ preview2.setAttribute(name, String(value));
6726
+ svg.append(preview2);
6727
+ this.#drag = {
6728
+ pointer: event.pointerId,
6729
+ svg,
6730
+ page: page2,
6731
+ document: state.document,
6732
+ shapeId: shape.id,
6733
+ preview: preview2,
6734
+ shapePreview: createRotationPreview(state.document, page2, shape, source),
6735
+ startX: event.clientX,
6736
+ startY: event.clientY,
6737
+ center,
6738
+ rotate: createRotationDrag(center, start, -shape.rotation.angle * 180 / Math.PI, {
6739
+ unwrapped: true
6740
+ })
6741
+ };
6742
+ try {
6743
+ this.#drag.shapePreview.update(shape.rotation.angle);
6744
+ } catch (error) {
6745
+ this.#cancel();
6746
+ this.options.announce(editErrorMessage(error));
6747
+ return;
6748
+ }
6749
+ event.preventDefault();
6750
+ event.stopImmediatePropagation();
6751
+ this.viewport.setPointerCapture?.(event.pointerId);
6752
+ }
6753
+ #move(event) {
6754
+ const drag = this.#drag;
6755
+ if (!drag || event.pointerId !== drag.pointer) return;
6756
+ const point = pagePoint(drag.svg, drag.page, event, pointOptions2);
6757
+ if (point) {
6758
+ const angle = -drag.rotate(point) * Math.PI / 180;
6759
+ try {
6760
+ drag.shapePreview.update(angle);
6761
+ } catch (error) {
6762
+ this.#cancel();
6763
+ this.options.announce(editErrorMessage(error));
6764
+ }
6765
+ drag.preview.setAttribute("x2", String(point.x));
6766
+ drag.preview.setAttribute("y2", String(point.y));
6767
+ }
6768
+ event.preventDefault();
6769
+ event.stopImmediatePropagation();
6770
+ }
6771
+ #cancel() {
6772
+ const drag = this.#drag;
6773
+ this.#drag = void 0;
6774
+ drag?.preview.remove();
6775
+ drag?.shapePreview.dispose();
6776
+ if (drag && this.viewport.hasPointerCapture?.(drag.pointer))
6777
+ this.viewport.releasePointerCapture(drag.pointer);
6778
+ }
6779
+ async #finish(event) {
6780
+ const drag = this.#drag;
6781
+ if (!drag || event.pointerId !== drag.pointer) return;
6782
+ event.preventDefault();
6783
+ event.stopImmediatePropagation();
6784
+ const point = pagePoint(drag.svg, drag.page, event, pointOptions2);
6785
+ const angle = point ? -drag.rotate(point) * Math.PI / 180 : void 0;
6786
+ this.#cancel();
6787
+ if (angle === void 0 || Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) < 0.5 || this.controller.state.document !== drag.document)
6788
+ return;
6789
+ const request = ++this.#request;
6790
+ try {
6791
+ await this.controller.applyEdits([
6792
+ { type: "rotate-shape", pageId: drag.page.id, shapeId: drag.shapeId, angle }
6793
+ ]);
6794
+ if (request === this.#request) this.options.announce("Shape rotated.");
6795
+ } catch (error) {
6796
+ if (request === this.#request && !isEditCancellation(error))
6797
+ this.options.announce(editErrorMessage(error));
6798
+ }
6799
+ }
6800
+ };
6801
+
6184
6802
  // src/visio/office-ui.ts
6185
6803
  var VISIO_ICONS = {
6186
6804
  visioLayers: "m2.5 7 7.5-4 7.5 4-7.5 4ZM2.5 10.5l7.5 4 7.5-4M2.5 14l7.5 4 7.5-4",
@@ -6289,14 +6907,15 @@ function estimatePageSvgBytes(model, page2, maxBytes) {
6289
6907
  reserve(
6290
6908
  4096 + geometry.path.length * 6 + shape.style.lineColor.length * 24 + shape.style.fill.length * 6
6291
6909
  );
6292
- const gradients = visioGradientInstances(shape.style.fillGradient);
6293
- reserve(1024 * gradients);
6294
- for (const stop of shape.style.fillGradient?.stops ?? [])
6295
- reserve(gradients * (256 + stop.color.length * 6));
6296
- if (shape.style.fillGradient)
6297
- reserve(
6298
- gradients * (visioRenderedGradientStopCount(shape.style.fillGradient) - shape.style.fillGradient.stops.length) * (256 + 7 * 6)
6299
- );
6910
+ for (const gradient of [shape.style.fillGradient, shape.style.lineGradient]) {
6911
+ const gradients = visioGradientInstances(gradient);
6912
+ reserve(1024 * gradients);
6913
+ for (const stop of gradient?.stops ?? []) reserve(gradients * (256 + stop.color.length * 6));
6914
+ if (gradient)
6915
+ reserve(
6916
+ gradients * (visioRenderedGradientStopCount(gradient) - gradient.stops.length) * (256 + 7 * 6)
6917
+ );
6918
+ }
6300
6919
  if (shape.style.fillPattern)
6301
6920
  reserve(1536 + 4 * Math.ceil(shape.style.fillPattern.bytes.byteLength / 3));
6302
6921
  if (shape.image) {
@@ -6593,14 +7212,16 @@ function validationCost(model) {
6593
7212
  );
6594
7213
  for (const geometry of shape.geometry) strings(geometry.path);
6595
7214
  if (shape.style.fillPattern) work += 64;
6596
- const gradients = visioGradientInstances(shape.style.fillGradient);
6597
- work += gradients * 16;
6598
- for (const stop of shape.style.fillGradient?.stops ?? []) {
6599
- work += 3 * gradients;
6600
- strings(stop.color);
7215
+ for (const gradient of [shape.style.fillGradient, shape.style.lineGradient]) {
7216
+ const gradients = visioGradientInstances(gradient);
7217
+ work += gradients * 16;
7218
+ for (const stop of gradient?.stops ?? []) {
7219
+ work += 3 * gradients;
7220
+ strings(stop.color);
7221
+ }
7222
+ if (gradient)
7223
+ work += gradients * 10 * (visioRenderedGradientStopCount(gradient) - gradient.stops.length);
6601
7224
  }
6602
- if (shape.style.fillGradient)
6603
- work += gradients * 10 * (visioRenderedGradientStopCount(shape.style.fillGradient) - shape.style.fillGradient.stops.length);
6604
7225
  for (const run of shape.text.runs) {
6605
7226
  work += 8;
6606
7227
  strings(run.text, run.fontFamily, run.color);
@@ -6642,6 +7263,9 @@ var VisioViewerElement = class extends BaseElement {
6642
7263
  #shapeStatus;
6643
7264
  #announcement;
6644
7265
  #commands;
7266
+ #lineEndpoints;
7267
+ #rotationHandle;
7268
+ #handleState;
6645
7269
  #rulers;
6646
7270
  #canvas;
6647
7271
  #panZoom;
@@ -6713,6 +7337,10 @@ var VisioViewerElement = class extends BaseElement {
6713
7337
  root: this.#root,
6714
7338
  viewport: this.#viewport,
6715
7339
  controller: this.controller,
7340
+ toolChanged: () => {
7341
+ this.#lineEndpoints.render(this.controller.state);
7342
+ this.#rotationHandle.render(this.controller.state);
7343
+ },
6716
7344
  fit: (mode) => this.#fit(mode),
6717
7345
  togglePane: (pane) => this.#chrome.togglePane(pane),
6718
7346
  reveal: (panel, focusText) => this.#chrome.reveal(panel, focusText),
@@ -6728,6 +7356,20 @@ var VisioViewerElement = class extends BaseElement {
6728
7356
  }
6729
7357
  });
6730
7358
  this.#profile = new ViewerProfile(this.#root);
7359
+ this.#lineEndpoints = new ViewerLineEndpoints(this.#viewport, this.controller, {
7360
+ active: () => this.#commands.tool === "pointer",
7361
+ announce: (message) => {
7362
+ this.#announcement = message;
7363
+ this.#status.textContent = message;
7364
+ }
7365
+ });
7366
+ this.#rotationHandle = new ViewerRotationHandle(this.#viewport, this.controller, {
7367
+ active: () => this.#commands.tool === "pointer",
7368
+ announce: (message) => {
7369
+ this.#announcement = message;
7370
+ this.#status.textContent = message;
7371
+ }
7372
+ });
6731
7373
  this.#share = new ViewerShare(this.#root, this.controller, () => this.#profile.profile);
6732
7374
  this.#backstage = new ViewerBackstage({
6733
7375
  root: this.#root,
@@ -6911,6 +7553,8 @@ var VisioViewerElement = class extends BaseElement {
6911
7553
  #wireInputs() {
6912
7554
  const disposeChrome = this.#chrome.wire();
6913
7555
  const disposeCommands = this.#commands.wire();
7556
+ const disposeLineEndpoints = this.#lineEndpoints.wire();
7557
+ const disposeRotation = this.#rotationHandle.wire();
6914
7558
  const disposeBackstage = this.#backstage.wire();
6915
7559
  const disposeMenus = wireContextMenus(this.#root, this.#viewport, this.controller);
6916
7560
  const disposeTellMe = wireTellMe(this.#root);
@@ -6946,6 +7590,8 @@ var VisioViewerElement = class extends BaseElement {
6946
7590
  return () => {
6947
7591
  disposeChrome();
6948
7592
  disposeCommands();
7593
+ disposeLineEndpoints();
7594
+ disposeRotation();
6949
7595
  disposeBackstage();
6950
7596
  disposeMenus();
6951
7597
  disposeTellMe();
@@ -6968,6 +7614,12 @@ var VisioViewerElement = class extends BaseElement {
6968
7614
  if (this.#suspended) return;
6969
7615
  const page2 = state.document?.pages[state.pageIndex];
6970
7616
  const changed = this.#canvas.render(state);
7617
+ const previous = this.#handleState;
7618
+ this.#handleState = state;
7619
+ if (changed || !previous || previous.zoom !== state.zoom || previous.selectedShape !== state.selectedShape || previous.loading !== state.loading || previous.edit.busy !== state.edit.busy || previous.edit.sourceAvailable !== state.edit.sourceAvailable) {
7620
+ this.#lineEndpoints.render(state);
7621
+ this.#rotationHandle.render(state);
7622
+ }
6971
7623
  if (changed) this.#announcement = void 0;
6972
7624
  this.#zoomSlider.value = Math.round(state.zoom * 100);
6973
7625
  this.#zoomSlider.disabled = !page2;