{"version":3,"file":"mindzie-bpmn-editor.umd.cjs","sources":["C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/core/errors.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/data-association-ends.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/label-pin.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/unique-id.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/routing/geometry.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/routing/astar.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/routing/elbow.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/routing/route.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/routing/anchors.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/core/commands.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/base.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/artifacts.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/participant-scope.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/tier-d.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/helpers.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/start-event-required.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/end-event-required.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/single-blank-start-event.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/no-disconnected.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/dead-end.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/unreachable.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/dangling-flow.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/duplicate-id.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/no-implicit-start.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/no-implicit-end.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/no-implicit-split.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/fake-join.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/no-gateway-join-fork.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/superfluous-gateway.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/superfluous-termination.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/no-duplicate-sequence-flows.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/no-complex-gateway.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/no-inclusive-gateway.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/event-based-gateway.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/sub-process-blank-start-event.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/event-sub-process-typed-start-event.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/conditional-flows.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/label-required.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/gateway-mismatch.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/verb-object-activity-label.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/past-participle-event-label.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/decompose-large-process.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/rules/minimize-routing-paths.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/catalogue.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/index.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/outline.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/search.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/diff.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/routing/obstacles.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/routing/quality.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/scene.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/primitives.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/renderers/labels.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/routing/separate.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/text-metrics.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/auto-layout/cycle-break.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/auto-layout/crossing-reduction.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/auto-layout/y-placement.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/auto-layout/index.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/i18n/messages/en.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/i18n/messages/de.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/i18n/messages/fr.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/i18n/messages/es.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/i18n/messages/nl.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/i18n/messages/tr.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/i18n/messages/ja.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/i18n/messages/da.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/i18n/messages/index.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/i18n/translator.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/minimap.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/quick-pattern-gallery.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/report-panel.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/palette-icons.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/palette-drag.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/palette-arm.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/palette.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/controls.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/lint/render.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/fast-xml-parser/src/util.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/fast-xml-parser/src/validator.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/fast-xml-parser/src/xmlparser/OptionsBuilder.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/fast-xml-parser/src/xmlparser/xmlNode.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/fast-xml-parser/src/xmlparser/DocTypeReader.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/strnum/strnum.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/fast-xml-parser/src/ignoreAttributes.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/fast-xml-parser/src/xmlparser/node2json.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/fast-xml-parser/src/xmlparser/XMLParser.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/fast-xml-parser/src/xmlbuilder/orderedJs2Xml.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/fast-xml-parser/src/xmlbuilder/json2xml.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/fast-xml-parser/src/fxp.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/io/parser.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/io/serializer.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/append-menu.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/lint-indicators.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/io/export-shared.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/io/export-svg.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/io/export-png.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/@babel/runtime/helpers/esm/typeof.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/fflate/esm/browser.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/jspdf/dist/jspdf.es.min.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/io/export-pdf.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/io/export-docs.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/layout.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/theme/tokens.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/theme/themes.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/theme/index.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/timer.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/message.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/signal.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/error.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/escalation.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/conditional.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/compensation-icon.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/cancel.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/link.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/terminate.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/multiple.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/parallel-multiple.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/user-task.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/service-task.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/send-task.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/receive-task.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/manual-task.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/business-rule-task.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/script-task.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/icons/index.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/renderers/events.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/renderers/markers.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/renderers/tasks.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/renderers/gateways.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/renderers/flows.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/renderers/subprocesses.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/renderers/data.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/renderers/artifacts.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/renderers/pools-lanes.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/renderers/boundary-events.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/a11y.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/renderers/index.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/viewport.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/warnings-overlay.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/property-panel.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/marquee.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/snap.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/shape-drag.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/connect-overlay.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/connect-drag.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/flow-endpoint-overlay.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/flow-endpoint-drag.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/resize-overlay.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/resize-drag.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/in-place-label-editor.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/bendpoint-edit.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/label-drag.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/lane-resize.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/context-pad.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/core/selection.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/view/pipeline.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/api/BpmnEditor.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/io/canonical.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/model/validator.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/src/index.ts","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/html2canvas/dist/html2canvas.esm.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/dompurify/dist/purify.es.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/global-this.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/fails.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/descriptors.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/function-bind-native.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/function-call.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/object-property-is-enumerable.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/create-property-descriptor.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/function-uncurry-this.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/classof-raw.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/indexed-object.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/is-null-or-undefined.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/require-object-coercible.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/to-indexed-object.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/is-callable.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/is-object.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/get-built-in.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/object-is-prototype-of.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/environment-user-agent.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/environment-v8-version.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/symbol-constructor-detection.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/use-symbol-as-uid.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/is-symbol.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/try-to-string.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/a-callable.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/get-method.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/ordinary-to-primitive.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/define-global-property.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/shared-store.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/shared.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/to-object.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/has-own-property.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/uid.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/well-known-symbol.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/to-primitive.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/to-property-key.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/document-create-element.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/ie8-dom-define.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/object-get-own-property-descriptor.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/v8-prototype-define-bug.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/an-object.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/object-define-property.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/create-non-enumerable-property.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/function-name.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/inspect-source.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/weak-map-basic-detection.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/shared-key.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/hidden-keys.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/internal-state.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/make-built-in.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/define-built-in.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/math-trunc.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/to-integer-or-infinity.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/to-absolute-index.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/to-length.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/length-of-array-like.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/array-includes.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/object-keys-internal.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/enum-bug-keys.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/object-get-own-property-names.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/object-get-own-property-symbols.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/own-keys.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/copy-constructor-properties.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/is-forced.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/export.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/environment.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/environment-is-node.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/path.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/function-uncurry-this-accessor.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/is-possible-prototype.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/a-possible-prototype.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/object-set-prototype-of.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/set-to-string-tag.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/define-built-in-accessor.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/set-species.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/an-instance.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/to-string-tag-support.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/classof.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/is-constructor.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/a-constructor.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/species-constructor.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/function-apply.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/function-uncurry-this-clause.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/function-bind-context.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/html.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/array-slice.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/validate-arguments-length.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/environment-is-ios.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/task.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/safe-get-built-in.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/queue.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/environment-is-ios-pebble.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/environment-is-webos-webkit.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/microtask.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/host-report-errors.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/perform.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/promise-native-constructor.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/promise-constructor-detection.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/new-promise-capability.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.promise.constructor.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/iterators.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/is-array-iterator-method.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/get-iterator-method.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/get-iterator.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/iterator-close.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/iterate.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/check-correctness-of-iteration.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/promise-statics-incorrect-iteration.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.promise.all.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.promise.catch.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.promise.race.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.promise.reject.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/promise-resolve.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.promise.resolve.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/to-string.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/regexp-flags.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/regexp-sticky-helpers.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/object-keys.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/object-define-properties.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/object-create.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/regexp-unsupported-dot-all.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/regexp-unsupported-ncg.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/regexp-exec.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.regexp.exec.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/fix-regexp-well-known-symbol-logic.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/string-multibyte.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/advance-string-index.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/regexp-flags-detection.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/regexp-get-flags.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/regexp-exec-abstract.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.string.match.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/get-substitution.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.string.replace.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/is-regexp.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/not-a-regexp.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/correct-is-regexp-logic.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.string.starts-with.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/add-to-unscopables.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/correct-prototype-getter.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/object-get-prototype-of.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/iterators-core.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/iterator-create-constructor.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/iterator-define.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/create-iter-result-object.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.array.iterator.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/dom-iterables.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/dom-token-list-prototype.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/web.dom-collections.iterator.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/@babel/runtime/helpers/esm/toPrimitive.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/@babel/runtime/helpers/esm/toPropertyKey.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/@babel/runtime/helpers/esm/defineProperty.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/array-reduce.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/array-method-is-strict.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.array.reduce.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.string.ends-with.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.string.split.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/performance-now/lib/performance-now.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/raf/index.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/whitespaces.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/string-trim.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/string-trim-forced.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.string.trim.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/rgbcolor/index.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.array.index-of.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.string.includes.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/internals/is-array.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.array.reverse.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/svg-pathdata/lib/SVGPathData.module.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/core-js/modules/es.regexp.to-string.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/stackblur-canvas/dist/stackblur-es.js","C:/AgentWork/N_Net8_1/1/s/mindzieBpmnStudio/mindzie.BpmnEditor/node_modules/canvg/lib/index.es.js"],"sourcesContent":["/**\r\n * mindzie.BpmnEditor — error model\r\n * Copyright (c) mindzie 2026 — proprietary, all rights reserved\r\n *\r\n * Implements: BpmnEditorError class with structured error codes per\r\n * IMPLEMENTATION_SPEC.md Section 5 and Section 13.4.3. Used by parser,\r\n * serializer (Phase 2), and renderer (Phase 3) to surface failures with\r\n * a stable, machine-readable contract.\r\n * Design influences: original; conventional Error subclassing pattern.\r\n * Code enum derived directly from Section 5 of the spec.\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\n/**\r\n * Stable error codes surfaced to callers of the public API.\r\n * Locked by Section 5 of the implementation spec — do not extend without\r\n * a corresponding spec amendment.\r\n */\r\nexport type BpmnEditorErrorCode =\r\n | 'PARSE_ERROR'\r\n | 'RENDER_ERROR'\r\n | 'INVALID_XML'\r\n | 'UNSUPPORTED_VERSION'\r\n | 'INTERNAL';\r\n\r\n/**\r\n * Single error type the library throws for any failure consumers should\r\n * branch on. Callers use the `code` field to discriminate; `detail` carries\r\n * optional structured context (offsets, element IDs, raw inner errors).\r\n */\r\nexport class BpmnEditorError extends Error {\r\n public readonly code: BpmnEditorErrorCode;\r\n public readonly detail?: unknown;\r\n\r\n public constructor(code: BpmnEditorErrorCode, message: string, detail?: unknown) {\r\n super(message);\r\n this.name = 'BpmnEditorError';\r\n this.code = code;\r\n if (detail !== undefined) {\r\n this.detail = detail;\r\n }\r\n // Preserve a clean stack on V8/Node and modern Chromium.\r\n const errorCtor = Error as unknown as {\r\n captureStackTrace?: (target: object, ctor?: new (...args: never[]) => unknown) => void;\r\n };\r\n if (typeof errorCtor.captureStackTrace === 'function') {\r\n errorCtor.captureStackTrace(this, BpmnEditorError);\r\n }\r\n }\r\n\r\n /** Malformed or unparseable XML (well-formedness errors). */\r\n public static invalidXml(message: string, detail?: unknown): BpmnEditorError {\r\n return new BpmnEditorError('INVALID_XML', message, detail);\r\n }\r\n\r\n /** XML parsed but is not BPMN 2.0 (wrong root, wrong namespace). */\r\n public static unsupportedVersion(message: string, detail?: unknown): BpmnEditorError {\r\n return new BpmnEditorError('UNSUPPORTED_VERSION', message, detail);\r\n }\r\n\r\n /** XML is well-formed BPMN but the parser cannot honor a required slot. */\r\n public static parseError(message: string, detail?: unknown): BpmnEditorError {\r\n return new BpmnEditorError('PARSE_ERROR', message, detail);\r\n }\r\n\r\n /** Rendering failure (Phase 3). Reserved here for symmetry. */\r\n public static renderError(message: string, detail?: unknown): BpmnEditorError {\r\n return new BpmnEditorError('RENDER_ERROR', message, detail);\r\n }\r\n\r\n /** Internal invariant violation; indicates a bug in the library. */\r\n public static internal(message: string, detail?: unknown): BpmnEditorError {\r\n return new BpmnEditorError('INTERNAL', message, detail);\r\n }\r\n}\r\n","/**\r\n * mindzie.BpmnEditor — data association end resolution\r\n * Copyright (c) mindzie 2026 — proprietary, all rights reserved\r\n *\r\n * Implements: which two shapes a `` or\r\n * `` connects on the canvas (#9580).\r\n *\r\n * A sequence flow names both of its ends in `sourceRef` / `targetRef`.\r\n * A data association does not: it is nested INSIDE the activity or event\r\n * it belongs to, and that owner is one of its two visible ends without\r\n * being named anywhere on the association itself.\r\n *\r\n * dataInputAssociation sourceRef = the data objects / stores (a list)\r\n * targetRef = the owner's dataInput, when present\r\n * visible: data item -> owner\r\n * dataOutputAssociation sourceRef = the owner's dataOutput, when present\r\n * targetRef = the data object / store\r\n * visible: owner -> data item\r\n *\r\n * The owner's own `dataInput` / `dataOutput` ids live inside its\r\n * ioSpecification and have no shape, so they are never the visible end.\r\n * The owner is found by walking the tree, because the document keeps no\r\n * parent index.\r\n *\r\n * An input association may list several sources but is drawn as ONE\r\n * line. The visible source is the listed item whose shape the line\r\n * starts at - the one nearest the first DI waypoint - not simply the\r\n * first in the list.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { BpmnDocument, BpmnElement } from './base.js';\r\n\r\n/** The ids of the two shapes an edge connects on the canvas. */\r\nexport interface EdgeEnds {\r\n sourceRef: string | undefined;\r\n targetRef: string | undefined;\r\n}\r\n\r\ninterface Point {\r\n x: number;\r\n y: number;\r\n}\r\n\r\ninterface Bounds {\r\n x: number;\r\n y: number;\r\n width: number;\r\n height: number;\r\n}\r\n\r\ninterface DiIndex {\r\n shapeBounds: Map;\r\n firstWaypoint: Map;\r\n}\r\n\r\ninterface OwnedAssociation {\r\n owner: BpmnElement;\r\n association: BpmnElement & { sourceRef?: string[]; targetRef?: string };\r\n kind: 'input' | 'output';\r\n}\r\n\r\n/** True for the two association types that hang off an activity or event. */\r\nexport function isDataAssociationType(type: string): boolean {\r\n return type === 'dataInputAssociation' || type === 'dataOutputAssociation';\r\n}\r\n\r\n/**\r\n * Every data association in the document, keyed by its id, with the two\r\n * shape ids it visibly connects.\r\n */\r\nexport function collectDataAssociationEnds(doc: BpmnDocument): Map {\r\n const di = indexDi(doc);\r\n const ends = new Map();\r\n for (const { owner, association, kind } of collectOwnedAssociations(doc)) {\r\n if (kind === 'input') {\r\n ends.set(association.id, {\r\n sourceRef: visibleSource(association.id, association.sourceRef ?? [], di),\r\n targetRef: owner.id,\r\n });\r\n } else {\r\n ends.set(association.id, { sourceRef: owner.id, targetRef: association.targetRef });\r\n }\r\n }\r\n return ends;\r\n}\r\n\r\n/**\r\n * Every data association in the document, keyed by its id, with every\r\n * element id it depends on: its owner and each data item it names.\r\n * Deleting any of them leaves the association dangling, so it has to go\r\n * too.\r\n */\r\nexport function collectDataAssociationTouches(doc: BpmnDocument): Map> {\r\n const touches = new Map>();\r\n for (const { owner, association } of collectOwnedAssociations(doc)) {\r\n const ids = new Set([owner.id, ...(association.sourceRef ?? [])]);\r\n if (association.targetRef !== undefined) ids.add(association.targetRef);\r\n touches.set(association.id, ids);\r\n }\r\n return touches;\r\n}\r\n\r\n/**\r\n * The two shape ids any edge connects. Flows and plain associations name\r\n * them directly; a data association is looked up in `dataEnds`.\r\n */\r\nexport function resolveEdgeEnds(\r\n flow: BpmnElement & { sourceRef?: unknown; targetRef?: unknown },\r\n dataEnds: ReadonlyMap,\r\n): EdgeEnds {\r\n if (isDataAssociationType(flow.type)) {\r\n return dataEnds.get(flow.id) ?? { sourceRef: undefined, targetRef: undefined };\r\n }\r\n return {\r\n sourceRef: typeof flow.sourceRef === 'string' ? flow.sourceRef : undefined,\r\n targetRef: typeof flow.targetRef === 'string' ? flow.targetRef : undefined,\r\n };\r\n}\r\n\r\nfunction collectOwnedAssociations(doc: BpmnDocument): OwnedAssociation[] {\r\n const owned: OwnedAssociation[] = [];\r\n const visit = (element: BpmnElement): void => {\r\n const owner = element as BpmnElement & {\r\n dataInputAssociations?: BpmnElement[];\r\n dataOutputAssociations?: BpmnElement[];\r\n flowElements?: BpmnElement[];\r\n };\r\n for (const association of owner.dataInputAssociations ?? []) {\r\n owned.push({ owner: element, association, kind: 'input' });\r\n }\r\n for (const association of owner.dataOutputAssociations ?? []) {\r\n owned.push({ owner: element, association, kind: 'output' });\r\n }\r\n for (const child of owner.flowElements ?? []) {\r\n visit(child);\r\n }\r\n };\r\n for (const root of doc.rootElements) {\r\n visit(root);\r\n }\r\n return owned;\r\n}\r\n\r\n/**\r\n * The listed source the drawn line starts at. With one source (the usual\r\n * case) that is it; with several, the one whose shape lies nearest the\r\n * line's first waypoint. With no DI to decide by, the first listed.\r\n */\r\nfunction visibleSource(associationId: string, sources: string[], di: DiIndex): string | undefined {\r\n if (sources.length <= 1) return sources[0];\r\n const start = di.firstWaypoint.get(associationId);\r\n if (start === undefined) return sources[0];\r\n let best: string | undefined;\r\n let bestDistance = Number.POSITIVE_INFINITY;\r\n for (const id of sources) {\r\n const bounds = di.shapeBounds.get(id);\r\n if (bounds === undefined) continue;\r\n const distance = distanceToBounds(start, bounds);\r\n if (distance < bestDistance) {\r\n best = id;\r\n bestDistance = distance;\r\n }\r\n }\r\n return best ?? sources[0];\r\n}\r\n\r\nfunction distanceToBounds(point: Point, bounds: Bounds): number {\r\n const dx = Math.max(bounds.x - point.x, 0, point.x - (bounds.x + bounds.width));\r\n const dy = Math.max(bounds.y - point.y, 0, point.y - (bounds.y + bounds.height));\r\n return Math.hypot(dx, dy);\r\n}\r\n\r\nfunction indexDi(doc: BpmnDocument): DiIndex {\r\n const shapeBounds = new Map();\r\n const firstWaypoint = new Map();\r\n for (const planeElement of primaryPlaneElements(doc)) {\r\n const di = planeElement as BpmnElement & {\r\n bpmnElement?: string;\r\n bounds?: Bounds;\r\n waypoints?: Point[];\r\n };\r\n if (di.bpmnElement === undefined) continue;\r\n if (di.type === 'BPMNShape' && di.bounds !== undefined) {\r\n shapeBounds.set(di.bpmnElement, di.bounds);\r\n } else if (di.type === 'BPMNEdge') {\r\n const first = di.waypoints?.[0];\r\n if (first !== undefined) firstWaypoint.set(di.bpmnElement, first);\r\n }\r\n }\r\n return { shapeBounds, firstWaypoint };\r\n}\r\n\r\n/**\r\n * The plane elements of the primary diagram - the first BPMNDiagram with\r\n * a real plane, the same rule the layout uses to pick what it draws.\r\n * Drill-down planes are never read: a shape that exists only there is not\r\n * on the canvas, so it must not decide which source a drawn line starts at.\r\n */\r\nfunction primaryPlaneElements(doc: BpmnDocument): BpmnElement[] {\r\n for (const diagram of doc.diagrams) {\r\n if (diagram.type !== 'BPMNDiagram') continue;\r\n const plane = (\r\n diagram as BpmnElement & {\r\n plane?: BpmnElement & { planeElements?: BpmnElement[] };\r\n }\r\n ).plane;\r\n if (plane?.type === 'BPMNPlane' && Array.isArray(plane.planeElements)) {\r\n return plane.planeElements;\r\n }\r\n }\r\n return [];\r\n}\r\n","/**\r\n * mindzie.BpmnEditor — manually-placed label pin\r\n * Copyright (c) mindzie 2026 — proprietary, all rights reserved\r\n *\r\n * Implements: the \"this label was placed by a person, leave it alone\"\r\n * marker (#8159). A user who drags a connector label into a readable\r\n * spot must not have that placement silently destroyed by the next\r\n * Auto layout pass, so the placement is recorded IN THE DOCUMENT\r\n * rather than in editor session state.\r\n *\r\n * The marker is a foreign attribute on the ``:\r\n *\r\n * \r\n * \r\n * \r\n *\r\n * That is standards-legal — BPMNDI's `DiagramElement` declares\r\n * ``, so a foreign-namespaced\r\n * attribute validates and is ignored by other vendors' tools — and it\r\n * round-trips through parser/serializer on the existing\r\n * `foreignAttributes` machinery. Persisting it (rather than keeping an\r\n * in-memory set like `manualFlowRoutes`) is what lets the placement\r\n * survive save/reload AND lets a layout engine outside this package\r\n * (the AI-job layout stage, #8224) honour the same pin.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { ForeignAttribute } from './base.js';\r\n\r\n/**\r\n * Namespace URI for mindzie's BPMN DI extensions. Versioned by date in\r\n * the OMG style so a future incompatible extension can take a new URI\r\n * without breaking documents written today.\r\n */\r\nexport const MINDZIE_DI_NS = 'http://mindzie.com/spec/BPMN/20260101/DI';\r\n\r\n/** Preferred prefix for {@link MINDZIE_DI_NS} when writing XML. */\r\nexport const MINDZIE_DI_PREFIX = 'mindzie';\r\n\r\n/** Local name of the placement-ownership attribute on a BPMNLabel. */\r\nexport const LABEL_PLACEMENT_ATTR = 'labelPlacement';\r\n\r\n/** Attribute value meaning \"a person put this label here\". */\r\nexport const LABEL_PLACEMENT_MANUAL = 'manual';\r\n\r\n/** The minimal shape this module needs off a BPMNLabel. */\r\ninterface LabelLike {\r\n foreignAttributes: ForeignAttribute[];\r\n}\r\n\r\n/**\r\n * True when `label` carries the manual-placement pin. Missing label,\r\n * missing attribute, or any other value all mean \"not pinned\", so a\r\n * document from another tool (which never has the attribute) reads as\r\n * auto-placed and stays fully under layout's control.\r\n */\r\nexport function isLabelManuallyPlaced(label: LabelLike | undefined | null): boolean {\r\n if (!label) return false;\r\n return label.foreignAttributes.some(\r\n (attr) =>\r\n attr.namespaceUri === MINDZIE_DI_NS &&\r\n attr.localName === LABEL_PLACEMENT_ATTR &&\r\n attr.value === LABEL_PLACEMENT_MANUAL,\r\n );\r\n}\r\n\r\n/**\r\n * Returns a NEW attribute array with the placement pin set (`pinned`)\r\n * or removed. Pure — the input array is never mutated — because\r\n * SetEdgeLabelBoundsCommand snapshots the previous array for undo and\r\n * an in-place edit would corrupt that snapshot.\r\n */\r\nexport function withLabelPlacementPin(\r\n attributes: readonly ForeignAttribute[],\r\n pinned: boolean,\r\n): ForeignAttribute[] {\r\n const without = attributes.filter(\r\n (attr) => !(attr.namespaceUri === MINDZIE_DI_NS && attr.localName === LABEL_PLACEMENT_ATTR),\r\n );\r\n if (!pinned) return without;\r\n without.push({\r\n namespaceUri: MINDZIE_DI_NS,\r\n localName: LABEL_PLACEMENT_ATTR,\r\n prefix: MINDZIE_DI_PREFIX,\r\n value: LABEL_PLACEMENT_MANUAL,\r\n });\r\n return without;\r\n}\r\n","/**\r\n * mindzie.BpmnEditor — the single id generator for new elements\r\n * Copyright (c) mindzie 2026 — proprietary, all rights reserved\r\n *\r\n * Implements: #8064. Every id minted for a NEWLY created element goes\r\n * through `uniqueElementId`, which owns the separator between the stem\r\n * and the ordinal. Callers pass a BARE stem (`Participant`, `Flow`,\r\n * `task`) and get `Participant_1`, `Flow_1`, `task_1`.\r\n *\r\n * Why this exists as one shared function: three generators had grown up\r\n * independently — `uniqueId` in `api/BpmnEditor.ts`, `uniqueIdInDoc` in\r\n * `core/commands.ts`, and a one-off inline loop — and only two of them\r\n * numbered from 1. The third seeded its candidate with the caller's base\r\n * and returned it verbatim when nothing collided, so callers compensated\r\n * by passing an underscore-terminated base. That produced ids ending in\r\n * a bare underscore (`Participant_`), and on the first collision a\r\n * DOUBLE underscore (`Participant__2`), because the ordinal was appended\r\n * with its own `_` to a base that already had one.\r\n *\r\n * These ids are written into the saved BPMN XML and are what other BPMN\r\n * tools key on, so the malformed forms are an interop problem, not a\r\n * cosmetic one.\r\n *\r\n * Scope note: this is for elements being CREATED. It never renames an id\r\n * that is already in a document — ids in existing files are opaque and\r\n * must round-trip byte-identically, malformed or not.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { BpmnDocument } from './base.js';\r\n\r\n/**\r\n * Mint an id of the form `_` that is not already present in\r\n * `doc`, with `n` starting at 1.\r\n *\r\n * @param doc the document whose `byId` index defines \"already taken\".\r\n * @param stem the bare stem, WITHOUT a trailing underscore — the\r\n * separator belongs to this function. Passing `'Lane_'`\r\n * would yield `Lane__1`.\r\n */\r\nexport function uniqueElementId(doc: BpmnDocument, stem: string): string {\r\n let n = 1;\r\n while (doc.byId.has(`${stem}_${String(n)}`)) n += 1;\r\n return `${stem}_${String(n)}`;\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - routing geometry helpers\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: small, pure geometry primitives used by the orthogonal\r\n * router. Points, axis-aligned rectangles, side selection, side ports,\r\n * and orthogonal-segment / rect intersection tests.\r\n *\r\n * Kept deliberately minimal so the router can be tested in isolation.\r\n * No scene or DI types escape this file.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nexport interface Point {\r\n x: number;\r\n y: number;\r\n}\r\n\r\nexport interface Rect {\r\n x: number;\r\n y: number;\r\n width: number;\r\n height: number;\r\n}\r\n\r\nexport type Side = 'top' | 'bottom' | 'left' | 'right';\r\n\r\nexport function rectCenter(r: Rect): Point {\r\n return { x: r.x + r.width / 2, y: r.y + r.height / 2 };\r\n}\r\n\r\n/** Returns a new rect expanded by `padding` on every side. */\r\nexport function rectInflate(r: Rect, padding: number): Rect {\r\n return {\r\n x: r.x - padding,\r\n y: r.y - padding,\r\n width: r.width + padding * 2,\r\n height: r.height + padding * 2,\r\n };\r\n}\r\n\r\n/**\r\n * Picks the side of `from` that faces `to`. Uses axis-dominance so a\r\n * partner directly right/left favours the horizontal side, directly\r\n * above/below favours the vertical. Ties break toward the horizontal\r\n * axis (arbitrary but stable).\r\n */\r\nexport function sideToward(from: Rect, to: Point): Side {\r\n const c = rectCenter(from);\r\n const dx = to.x - c.x;\r\n const dy = to.y - c.y;\r\n if (Math.abs(dx) >= Math.abs(dy)) {\r\n return dx >= 0 ? 'right' : 'left';\r\n }\r\n return dy >= 0 ? 'bottom' : 'top';\r\n}\r\n\r\n/**\r\n * Returns true when `point` lies in the half-plane that the named side\r\n * of `r` faces outward into. Used to detect whether a side selection\r\n * (typically chosen by `sideToward` from the immediate neighbour\r\n * waypoint) actually faces toward the other end of the flow, or whether\r\n * it faces away — the latter is the symptom of a stale interior bend\r\n * after several drags.\r\n */\r\nexport function sideFacesPoint(r: Rect, side: Side, point: Point): boolean {\r\n switch (side) {\r\n case 'top':\r\n return point.y < r.y;\r\n case 'bottom':\r\n return point.y > r.y + r.height;\r\n case 'left':\r\n return point.x < r.x;\r\n case 'right':\r\n return point.x > r.x + r.width;\r\n }\r\n}\r\n\r\n/** Midpoint of the named side of `r`. */\r\nexport function sidePort(r: Rect, side: Side): Point {\r\n const cx = r.x + r.width / 2;\r\n const cy = r.y + r.height / 2;\r\n switch (side) {\r\n case 'top':\r\n return { x: cx, y: r.y };\r\n case 'bottom':\r\n return { x: cx, y: r.y + r.height };\r\n case 'left':\r\n return { x: r.x, y: cy };\r\n case 'right':\r\n return { x: r.x + r.width, y: cy };\r\n }\r\n}\r\n\r\n/** Push `p` outward perpendicular to `side` by `distance` units. */\r\nexport function offsetFromSide(p: Point, side: Side, distance: number): Point {\r\n switch (side) {\r\n case 'top':\r\n return { x: p.x, y: p.y - distance };\r\n case 'bottom':\r\n return { x: p.x, y: p.y + distance };\r\n case 'left':\r\n return { x: p.x - distance, y: p.y };\r\n case 'right':\r\n return { x: p.x + distance, y: p.y };\r\n }\r\n}\r\n\r\n/**\r\n * True when the horizontal segment at height `y` from `x1` to `x2`\r\n * passes through the interior of `r`. Boundary touches (segment lies\r\n * exactly on a rect edge) do not count as intersection, which lets a\r\n * path slide along the outside of a padded obstacle.\r\n */\r\nexport function hSegmentIntersects(\r\n x1: number,\r\n x2: number,\r\n y: number,\r\n r: Rect,\r\n): boolean {\r\n if (y <= r.y || y >= r.y + r.height) return false;\r\n const lo = Math.min(x1, x2);\r\n const hi = Math.max(x1, x2);\r\n return hi > r.x && lo < r.x + r.width;\r\n}\r\n\r\n/** Vertical-segment twin of {@link hSegmentIntersects}. */\r\nexport function vSegmentIntersects(\r\n y1: number,\r\n y2: number,\r\n x: number,\r\n r: Rect,\r\n): boolean {\r\n if (x <= r.x || x >= r.x + r.width) return false;\r\n const lo = Math.min(y1, y2);\r\n const hi = Math.max(y1, y2);\r\n return hi > r.y && lo < r.y + r.height;\r\n}\r\n\r\n/**\r\n * Classifies which side of `r` the border point `p` docks on, or null\r\n * when the point is not on the border (a floating endpoint left\r\n * behind by drag history). Corner points classify as left/right\r\n * first. Used by the stale-route gate to detect flows that enter or\r\n * leave a shape through the side facing AWAY from their partner —\r\n * the signature of a backwards-routed edge.\r\n */\r\nexport function dockSide(r: Rect, p: Point, epsilon = 1): Side | null {\r\n const withinX = p.x >= r.x - epsilon && p.x <= r.x + r.width + epsilon;\r\n const withinY = p.y >= r.y - epsilon && p.y <= r.y + r.height + epsilon;\r\n if (Math.abs(p.x - r.x) <= epsilon && withinY) return 'left';\r\n if (Math.abs(p.x - (r.x + r.width)) <= epsilon && withinY) return 'right';\r\n if (Math.abs(p.y - r.y) <= epsilon && withinX) return 'top';\r\n if (Math.abs(p.y - (r.y + r.height)) <= epsilon && withinX) return 'bottom';\r\n return null;\r\n}\r\n\r\n/**\r\n * Cost of an orthogonal polyline: total Manhattan length plus a fixed\r\n * penalty per direction change. The same metric the A* search uses\r\n * (bend penalty default 10), exposed so route candidates and the\r\n * stale-route replacement gate in MoveShapeCommand compare paths on\r\n * one scale. Degenerate zero-length segments contribute nothing and\r\n * do not count as bends.\r\n */\r\nexport function routeCost(path: readonly Point[], bendPenalty = 10): number {\r\n let cost = 0;\r\n let prevHorizontal: boolean | null = null;\r\n for (let i = 1; i < path.length; i += 1) {\r\n const a = path[i - 1];\r\n const b = path[i];\r\n if (a === undefined || b === undefined) continue;\r\n const dx = Math.abs(b.x - a.x);\r\n const dy = Math.abs(b.y - a.y);\r\n if (dx === 0 && dy === 0) continue;\r\n cost += dx + dy;\r\n const horizontal = dx >= dy;\r\n if (prevHorizontal !== null && horizontal !== prevHorizontal) {\r\n cost += bendPenalty;\r\n }\r\n prevHorizontal = horizontal;\r\n }\r\n return cost;\r\n}\r\n\r\n/**\r\n * Removes waypoints that are collinear with their neighbours so the\r\n * returned list only contains real bends plus the two endpoints.\r\n * Pure — returns a fresh array with fresh Point objects.\r\n */\r\nexport function coalesceCollinear(path: readonly Point[]): Point[] {\r\n if (path.length <= 2) {\r\n return path.map((p) => ({ x: p.x, y: p.y }));\r\n }\r\n const out: Point[] = [];\r\n const first = path[0];\r\n if (first !== undefined) out.push({ x: first.x, y: first.y });\r\n for (let i = 1; i < path.length - 1; i += 1) {\r\n const prev = path[i - 1];\r\n const cur = path[i];\r\n const next = path[i + 1];\r\n if (prev === undefined || cur === undefined || next === undefined) continue;\r\n const alongH = prev.y === cur.y && cur.y === next.y;\r\n const alongV = prev.x === cur.x && cur.x === next.x;\r\n if (!alongH && !alongV) {\r\n out.push({ x: cur.x, y: cur.y });\r\n }\r\n }\r\n const last = path[path.length - 1];\r\n if (last !== undefined) out.push({ x: last.x, y: last.y });\r\n return out;\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - orthogonal A* on a Hanan grid\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: orthogonal-path search between two points that avoids a\r\n * set of axis-aligned rectangular obstacles. Uses the classic Hanan\r\n * grid - vertices formed by the Cartesian product of every unique x\r\n * and y value drawn from the endpoints and obstacle corners - which\r\n * provably contains at least one optimal rectilinear path (Hanan,\r\n * 1966). That keeps the graph sparse (O(n^2) nodes for n obstacles)\r\n * so a scene with dozens of shapes is a few-hundred-node A*.\r\n *\r\n * Cost per edge is Manhattan distance plus a fixed bend penalty on\r\n * direction change, which biases the search toward fewer turns - the\r\n * aesthetic most BPMN users expect. State is (node, arrival-direction)\r\n * so bend cost is computed exactly rather than approximated.\r\n *\r\n * Pure: no DOM, no model types, no scene lookups. `Rect` + `Point`\r\n * are the only vocabulary.\r\n *\r\n * Design influences: Hanan grid construction and orthogonal A* are\r\n * textbook algorithms; this file is an original implementation.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { Point, Rect } from './geometry.js';\r\nimport {\r\n coalesceCollinear,\r\n hSegmentIntersects,\r\n vSegmentIntersects,\r\n} from './geometry.js';\r\n\r\nexport interface AStarOptions {\r\n /**\r\n * Cost added whenever the path changes direction. Higher values\r\n * bias the router toward paths with fewer bends. Default 10 (scene\r\n * units), which reads well against typical shape sizes (36-100 px).\r\n */\r\n bendPenalty?: number;\r\n}\r\n\r\ntype Direction = 'h' | 'v' | null;\r\n\r\ninterface Node {\r\n col: number;\r\n row: number;\r\n dir: Direction;\r\n g: number;\r\n f: number;\r\n parent: Node | null;\r\n}\r\n\r\n/**\r\n * Finds a rectilinear (orthogonal) path from `source` to `target` that\r\n * avoids obstacle interiors. Returns the sequence of waypoints - the\r\n * first point is always `source`, the last is always `target`, and\r\n * intermediate points are bends. Collinear runs are coalesced so the\r\n * output has no redundant vertices.\r\n *\r\n * When no feasible path exists on the Hanan grid (source or target\r\n * inside an obstacle, or the obstacles completely enclose one of\r\n * them), falls through to a two-point straight line - callers can\r\n * detect this by checking `result.length === 2` and rendering as-is.\r\n */\r\nexport function computeAStarRoute(\r\n source: Point,\r\n target: Point,\r\n obstacles: readonly Rect[],\r\n options: AStarOptions = {},\r\n): Point[] {\r\n const bendPenalty = options.bendPenalty ?? 10;\r\n\r\n if (source.x === target.x && source.y === target.y) {\r\n return [source];\r\n }\r\n\r\n // Source / target inside any obstacle is infeasible.\r\n for (const o of obstacles) {\r\n if (pointInRectInterior(source, o) || pointInRectInterior(target, o)) {\r\n return [source, target];\r\n }\r\n }\r\n\r\n // Hanan grid: every x and y that appears on any endpoint or obstacle\r\n // corner. Sorted + de-duplicated so grid-cell indices are stable.\r\n const xSet = new Set([source.x, target.x]);\r\n const ySet = new Set([source.y, target.y]);\r\n for (const o of obstacles) {\r\n xSet.add(o.x);\r\n xSet.add(o.x + o.width);\r\n ySet.add(o.y);\r\n ySet.add(o.y + o.height);\r\n }\r\n const xs = [...xSet].sort((a, b) => a - b);\r\n const ys = [...ySet].sort((a, b) => a - b);\r\n const W = xs.length;\r\n const H = ys.length;\r\n\r\n // Flatten grid into a Point[][] so we can iterate without index\r\n // assertions (tsconfig: noUncheckedIndexedAccess).\r\n const grid: Point[][] = new Array(W);\r\n for (let c = 0; c < W; c++) {\r\n const xc = xs[c];\r\n const col = new Array(H);\r\n if (xc === undefined) {\r\n grid[c] = col;\r\n continue;\r\n }\r\n for (let r = 0; r < H; r++) {\r\n const yr = ys[r];\r\n col[r] = { x: xc, y: yr ?? 0 };\r\n }\r\n grid[c] = col;\r\n }\r\n\r\n const srcCol = xs.indexOf(source.x);\r\n const srcRow = ys.indexOf(source.y);\r\n const tgtCol = xs.indexOf(target.x);\r\n const tgtRow = ys.indexOf(target.y);\r\n if (srcCol < 0 || srcRow < 0 || tgtCol < 0 || tgtRow < 0) {\r\n return [source, target];\r\n }\r\n\r\n const pointAt = (col: number, row: number): Point => {\r\n const column = grid[col];\r\n if (column === undefined) return { x: 0, y: 0 };\r\n const p = column[row];\r\n return p ?? { x: 0, y: 0 };\r\n };\r\n\r\n const heuristic = (col: number, row: number): number => {\r\n const p = pointAt(col, row);\r\n return Math.abs(p.x - target.x) + Math.abs(p.y - target.y);\r\n };\r\n\r\n const edgeClear = (a: Point, b: Point): boolean => {\r\n if (a.x === b.x) {\r\n for (const o of obstacles) {\r\n if (vSegmentIntersects(a.y, b.y, a.x, o)) return false;\r\n }\r\n return true;\r\n }\r\n for (const o of obstacles) {\r\n if (hSegmentIntersects(a.x, b.x, a.y, o)) return false;\r\n }\r\n return true;\r\n };\r\n\r\n const open: Node[] = [];\r\n const closed = new Map();\r\n\r\n const pushNode = (n: Node): void => {\r\n // Stable-insertion sort into a small open list. N is O(W*H) which\r\n // for typical BPMN scenes is under ~200; a heap is not worth the\r\n // code complexity here.\r\n let lo = 0;\r\n let hi = open.length;\r\n while (lo < hi) {\r\n const mid = (lo + hi) >>> 1;\r\n const m = open[mid];\r\n if (m !== undefined && m.f <= n.f) lo = mid + 1;\r\n else hi = mid;\r\n }\r\n open.splice(lo, 0, n);\r\n };\r\n\r\n const makeKey = (col: number, row: number, dir: Direction): string =>\r\n col + ',' + row + ',' + (dir ?? 'x');\r\n\r\n const start: Node = {\r\n col: srcCol,\r\n row: srcRow,\r\n dir: null,\r\n g: 0,\r\n f: heuristic(srcCol, srcRow),\r\n parent: null,\r\n };\r\n pushNode(start);\r\n closed.set(makeKey(srcCol, srcRow, null), 0);\r\n\r\n const neighbours: { d: Direction; dc: number; dr: number }[] = [\r\n { d: 'h', dc: -1, dr: 0 },\r\n { d: 'h', dc: 1, dr: 0 },\r\n { d: 'v', dc: 0, dr: -1 },\r\n { d: 'v', dc: 0, dr: 1 },\r\n ];\r\n\r\n const reconstructPath = (end: Node): Point[] => {\r\n const nodes: Point[] = [];\r\n let n: Node | null = end;\r\n while (n !== null) {\r\n nodes.push(pointAt(n.col, n.row));\r\n n = n.parent;\r\n }\r\n nodes.reverse();\r\n return coalesceCollinear(nodes);\r\n };\r\n\r\n while (open.length > 0) {\r\n const cur = open.shift();\r\n if (cur === undefined) break;\r\n if (cur.col === tgtCol && cur.row === tgtRow) {\r\n return reconstructPath(cur);\r\n }\r\n const curKey = makeKey(cur.col, cur.row, cur.dir);\r\n const bestG = closed.get(curKey);\r\n if (bestG !== undefined && bestG < cur.g) {\r\n continue;\r\n }\r\n const curPoint = pointAt(cur.col, cur.row);\r\n for (const n of neighbours) {\r\n const nc = cur.col + n.dc;\r\n const nr = cur.row + n.dr;\r\n if (nc < 0 || nc >= W || nr < 0 || nr >= H) continue;\r\n const nPoint = pointAt(nc, nr);\r\n const segLen =\r\n Math.abs(nPoint.x - curPoint.x) + Math.abs(nPoint.y - curPoint.y);\r\n if (segLen === 0) continue;\r\n if (!edgeClear(curPoint, nPoint)) continue;\r\n const turnCost =\r\n cur.dir !== null && cur.dir !== n.d ? bendPenalty : 0;\r\n const g = cur.g + segLen + turnCost;\r\n const nKey = makeKey(nc, nr, n.d);\r\n const prev = closed.get(nKey);\r\n if (prev !== undefined && prev <= g) continue;\r\n closed.set(nKey, g);\r\n pushNode({\r\n col: nc,\r\n row: nr,\r\n dir: n.d,\r\n g,\r\n f: g + heuristic(nc, nr),\r\n parent: cur,\r\n });\r\n }\r\n }\r\n\r\n // No path found.\r\n return [source, target];\r\n}\r\n\r\nfunction pointInRectInterior(p: Point, r: Rect): boolean {\r\n return (\r\n p.x > r.x && p.x < r.x + r.width && p.y > r.y && p.y < r.y + r.height\r\n );\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - orthogonal elbow router\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: R3 fast path for computeOrthogonalRoute. Produces an\r\n * orthogonal polyline between two ports with 0, 1, or 2 bends\r\n * depending on which sides of the shapes the path exits / enters.\r\n * Used as the first-choice router in route.ts; the A* search only\r\n * runs when the elbow result collides with an obstacle.\r\n *\r\n * 16 side-combination cases reduce to four families:\r\n *\r\n * H-H (source exits L/R and target enters L/R):\r\n * straight (1 segment) when the ports are aligned within the\r\n * straight-snap tolerance (nearly-aligned rows are snapped onto\r\n * one row instead of producing a two-bend staircase);\r\n * otherwise a Z through a mid-x column (2 bends).\r\n *\r\n * V-V (source exits T/B and target enters T/B):\r\n * straight when the ports are aligned within the straight-snap\r\n * tolerance; otherwise a Z through a mid-y row (2 bends).\r\n *\r\n * H-V (source exits L/R, target enters T/B):\r\n * L-shape with elbow at (targetPort.x, sourcePort.y). 1 bend.\r\n *\r\n * V-H (source exits T/B, target enters L/R):\r\n * L-shape with elbow at (sourcePort.x, targetPort.y). 1 bend.\r\n *\r\n * \"Backward\" direction sanity: the first segment must exit the source\r\n * along the named side's normal. If the elbow would force it to\r\n * retreat through the shape (rare — only happens when side selection\r\n * was overridden by a caller), the function returns null so the\r\n * caller can fall back to A*.\r\n *\r\n * Pure: depends only on `Point` / `Rect` / `Side`.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { Point, Rect, Side } from './geometry.js';\r\nimport { sidePort } from './geometry.js';\r\n\r\n/**\r\n * Ports within this distance of each other on the cross axis are\r\n * treated as aligned and connected with a straight segment instead of\r\n * a two-bend Z. Matches the ~20 unit orientation threshold bpmn-js\r\n * uses for its h:h / v:v layouts. The effective tolerance shrinks for\r\n * small shapes (events) so the snapped dock never lands within\r\n * {@link DOCK_CORNER_MARGIN} of a shape corner.\r\n */\r\nconst ALIGN_TOLERANCE = 20;\r\nconst DOCK_CORNER_MARGIN = 4;\r\n\r\n/**\r\n * Effective straight-snap tolerance for a side pair whose shapes have\r\n * the given cross-axis extents (heights for H-H, widths for V-V).\r\n */\r\nfunction straightSnapTolerance(\r\n sourceExtent: number,\r\n targetExtent: number,\r\n): number {\r\n return Math.max(\r\n 0,\r\n Math.min(\r\n ALIGN_TOLERANCE,\r\n sourceExtent / 2 - DOCK_CORNER_MARGIN,\r\n targetExtent / 2 - DOCK_CORNER_MARGIN,\r\n ),\r\n );\r\n}\r\n\r\n/** Signed unit vector pointing outward from the named side. */\r\nfunction outwardUnit(side: Side): { dx: number; dy: number } {\r\n switch (side) {\r\n case 'left':\r\n return { dx: -1, dy: 0 };\r\n case 'right':\r\n return { dx: 1, dy: 0 };\r\n case 'top':\r\n return { dx: 0, dy: -1 };\r\n case 'bottom':\r\n return { dx: 0, dy: 1 };\r\n }\r\n}\r\n\r\n/** True when the named side is left or right (horizontal exit). */\r\nfunction isHorizontal(side: Side): boolean {\r\n return side === 'left' || side === 'right';\r\n}\r\n\r\n/**\r\n * Computes the elbow polyline from `source` via `sourceSide` to\r\n * `target` via `targetSide`. Returns null when the side selection\r\n * would require the first segment to retreat into the source shape\r\n * (caller should fall back to A* in that case).\r\n */\r\nexport function computeElbowRoute(\r\n source: Rect,\r\n target: Rect,\r\n sourceSide: Side,\r\n targetSide: Side,\r\n): Point[] | null {\r\n const sourcePort = sidePort(source, sourceSide);\r\n const targetPort = sidePort(target, targetSide);\r\n const sourceDir = outwardUnit(sourceSide);\r\n const targetDir = outwardUnit(targetSide);\r\n\r\n const sourceIsH = isHorizontal(sourceSide);\r\n const targetIsH = isHorizontal(targetSide);\r\n\r\n // H-H\r\n if (sourceIsH && targetIsH) {\r\n const crossOffset = targetPort.y - sourcePort.y;\r\n if (\r\n Math.abs(crossOffset) <=\r\n straightSnapTolerance(source.height, target.height)\r\n ) {\r\n // Straight horizontal. Nearly-aligned ports are snapped onto one\r\n // row rather than producing a two-bend staircase; the shared y\r\n // comes from the shape with the smaller side extent so the dock\r\n // stays dead-center where off-center docking would be most\r\n // visible (e.g. a 36-unit event vs an 80-unit task). Both exit\r\n // and entry directions must agree with the segment direction so\r\n // a caller-pinned far side falls through to A* instead of\r\n // cutting through a shape.\r\n const y = source.height <= target.height ? sourcePort.y : targetPort.y;\r\n const dx = targetPort.x - sourcePort.x;\r\n if (sourceDir.dx !== 0 && Math.sign(dx) !== sourceDir.dx) return null;\r\n if (targetDir.dx !== 0 && Math.sign(dx) !== -targetDir.dx) return null;\r\n return [\r\n { x: sourcePort.x, y },\r\n { x: targetPort.x, y },\r\n ];\r\n }\r\n const midX = (sourcePort.x + targetPort.x) / 2;\r\n // Source exit direction must agree with where midX sits relative\r\n // to sourcePort — otherwise the first segment would double back.\r\n if (sourceDir.dx !== 0 && Math.sign(midX - sourcePort.x) !== sourceDir.dx) {\r\n return null;\r\n }\r\n if (targetDir.dx !== 0 && Math.sign(midX - targetPort.x) !== targetDir.dx) {\r\n return null;\r\n }\r\n return [\r\n sourcePort,\r\n { x: midX, y: sourcePort.y },\r\n { x: midX, y: targetPort.y },\r\n targetPort,\r\n ];\r\n }\r\n\r\n // V-V\r\n if (!sourceIsH && !targetIsH) {\r\n const crossOffset = targetPort.x - sourcePort.x;\r\n if (\r\n Math.abs(crossOffset) <=\r\n straightSnapTolerance(source.width, target.width)\r\n ) {\r\n // Straight vertical — same near-alignment snap as H-H above.\r\n const x = source.width <= target.width ? sourcePort.x : targetPort.x;\r\n const dy = targetPort.y - sourcePort.y;\r\n if (sourceDir.dy !== 0 && Math.sign(dy) !== sourceDir.dy) return null;\r\n if (targetDir.dy !== 0 && Math.sign(dy) !== -targetDir.dy) return null;\r\n return [\r\n { x, y: sourcePort.y },\r\n { x, y: targetPort.y },\r\n ];\r\n }\r\n const midY = (sourcePort.y + targetPort.y) / 2;\r\n if (sourceDir.dy !== 0 && Math.sign(midY - sourcePort.y) !== sourceDir.dy) {\r\n return null;\r\n }\r\n if (targetDir.dy !== 0 && Math.sign(midY - targetPort.y) !== targetDir.dy) {\r\n return null;\r\n }\r\n return [\r\n sourcePort,\r\n { x: sourcePort.x, y: midY },\r\n { x: targetPort.x, y: midY },\r\n targetPort,\r\n ];\r\n }\r\n\r\n // H-V: L-shape, elbow at (targetPort.x, sourcePort.y).\r\n if (sourceIsH && !targetIsH) {\r\n const elbow: Point = { x: targetPort.x, y: sourcePort.y };\r\n // Degenerate: endpoints already share a row or column.\r\n if (elbow.x === sourcePort.x || elbow.y === targetPort.y) {\r\n return [sourcePort, targetPort];\r\n }\r\n if (\r\n sourceDir.dx !== 0 &&\r\n Math.sign(elbow.x - sourcePort.x) !== sourceDir.dx\r\n ) {\r\n return null;\r\n }\r\n if (\r\n targetDir.dy !== 0 &&\r\n Math.sign(elbow.y - targetPort.y) !== targetDir.dy\r\n ) {\r\n return null;\r\n }\r\n return [sourcePort, elbow, targetPort];\r\n }\r\n\r\n // V-H: L-shape, elbow at (sourcePort.x, targetPort.y).\r\n const elbow: Point = { x: sourcePort.x, y: targetPort.y };\r\n if (elbow.x === targetPort.x || elbow.y === sourcePort.y) {\r\n return [sourcePort, targetPort];\r\n }\r\n if (\r\n sourceDir.dy !== 0 &&\r\n Math.sign(elbow.y - sourcePort.y) !== sourceDir.dy\r\n ) {\r\n return null;\r\n }\r\n if (\r\n targetDir.dx !== 0 &&\r\n Math.sign(elbow.x - targetPort.x) !== targetDir.dx\r\n ) {\r\n return null;\r\n }\r\n return [sourcePort, elbow, targetPort];\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - high-level orthogonal router\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: M1 from IMPLEMENTATION_STATUS.md 3.1 - picks exit/entry\r\n * sides on source + target shapes, runs the Hanan A* over the\r\n * obstacle set, and returns a list of waypoints ready to drop into a\r\n * `BPMNEdge`.\r\n *\r\n * Public entry point: {@link computeOrthogonalRoute}.\r\n *\r\n * Fallback posture: when the search returns only two points (no path\r\n * found), the output is just [sourcePort, targetPort] - a straight\r\n * line. Callers render that as-is; a two-point line is valid BPMN DI.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { Point, Rect, Side } from './geometry.js';\r\nimport {\r\n rectCenter,\r\n sideToward,\r\n sidePort,\r\n offsetFromSide,\r\n coalesceCollinear,\r\n routeCost,\r\n hSegmentIntersects,\r\n vSegmentIntersects,\r\n} from './geometry.js';\r\nimport { computeAStarRoute } from './astar.js';\r\nimport { computeElbowRoute } from './elbow.js';\r\n\r\nexport interface RouteOptions {\r\n /**\r\n * Distance the first and last segments project out from the shape\r\n * edge before the router is free to turn. Prevents the route from\r\n * instantly doubling back through the source. Default 20 scene units.\r\n */\r\n endpointClearance?: number;\r\n /**\r\n * Bend penalty passed through to A*. Default: library default (10).\r\n */\r\n bendPenalty?: number;\r\n /**\r\n * Forces the exit side on the source shape instead of the natural\r\n * partner-facing choice. Used by the anchor-distribution pass\r\n * (#7898) to route gateway flows out of distinct diamond tips.\r\n */\r\n sourceSide?: Side;\r\n /** Forces the entry side on the target shape. See `sourceSide`. */\r\n targetSide?: Side;\r\n}\r\n\r\n/**\r\n * Returns waypoints for an orthogonal route from a port on `source`\r\n * to a port on `target`, avoiding the shapes in `otherShapes`. The\r\n * first and last waypoints are on the shape edges; intermediate\r\n * waypoints are the bends.\r\n *\r\n * Strategy (R3/R4): score fast elbow candidates over the plausible\r\n * side pairs first and take the cheapest obstacle-free one. Only when\r\n * no elbow candidate survives fall through to the Hanan-grid A*\r\n * search, which is slower but can route around dense obstacle fields.\r\n */\r\nexport function computeOrthogonalRoute(\r\n source: Rect,\r\n target: Rect,\r\n otherShapes: readonly Rect[],\r\n options: RouteOptions = {},\r\n): Point[] {\r\n const clearance = options.endpointClearance ?? 20;\r\n const bendPenalty = options.bendPenalty ?? 10;\r\n\r\n // Primary exit / entry sides: the partner-facing axis-dominant side,\r\n // unless the caller pinned a side (anchor distribution, #7898).\r\n const sourceSide = options.sourceSide ?? sideToward(source, rectCenter(target));\r\n const targetSide = options.targetSide ?? sideToward(target, rectCenter(source));\r\n\r\n const sourcePort = sidePort(source, sourceSide);\r\n const targetPort = sidePort(target, targetSide);\r\n\r\n // Fast path: candidate-scored elbow routing. The primary side pair\r\n // is not committed blindly — every plausible side pair (primary plus\r\n // the cross-axis side facing the partner, per end) produces an elbow\r\n // candidate; invalid (backward) and obstacle-crossing candidates are\r\n // discarded and the survivors compete on length + bend count. The\r\n // preference penalty keeps the conventional BPMN look (H-H Z between\r\n // offset shapes) whenever it is not materially worse, while letting\r\n // a clean one-bend L through a free side win over an A* detour when\r\n // the primary corridor is blocked. Pinned sides are respected — a\r\n // pinned end contributes exactly one candidate.\r\n const sourceCandidates: readonly Side[] =\r\n options.sourceSide !== undefined\r\n ? [options.sourceSide]\r\n : candidateSides(source, rectCenter(target));\r\n const targetCandidates: readonly Side[] =\r\n options.targetSide !== undefined\r\n ? [options.targetSide]\r\n : candidateSides(target, rectCenter(source));\r\n\r\n let best: Point[] | null = null;\r\n let bestCost = Infinity;\r\n for (const ss of sourceCandidates) {\r\n for (const ts of targetCandidates) {\r\n const elbow = computeElbowRoute(source, target, ss, ts);\r\n if (elbow === null) continue;\r\n if (pathIntersectsAny(elbow, otherShapes)) continue;\r\n const preferencePenalty =\r\n (ss === sourceSide ? 0 : SIDE_PREFERENCE_PENALTY) +\r\n (ts === targetSide ? 0 : SIDE_PREFERENCE_PENALTY);\r\n const cost = routeCost(elbow, bendPenalty) + preferencePenalty;\r\n if (cost < bestCost) {\r\n best = elbow;\r\n bestCost = cost;\r\n }\r\n }\r\n }\r\n if (best !== null) {\r\n return coalesceCollinear(best);\r\n }\r\n\r\n // Slow path: Hanan A* through clearance stubs off the primary ports.\r\n const sourceStub = offsetFromSide(sourcePort, sourceSide, clearance);\r\n const targetStub = offsetFromSide(targetPort, targetSide, clearance);\r\n\r\n const inner = computeAStarRoute(sourceStub, targetStub, otherShapes, {\r\n bendPenalty,\r\n });\r\n\r\n // Merge: sourcePort -> inner[0..] -> targetPort. If inner has\r\n // exactly 2 points (stub pair), the simple path is port-stub-stub-\r\n // port; coalesce collinear so the output is clean.\r\n const combined: Point[] = [sourcePort, ...inner, targetPort];\r\n return coalesceCollinear(combined);\r\n}\r\n\r\n/**\r\n * Cost added per side that deviates from the primary (partner-facing,\r\n * axis-dominant) choice when scoring elbow candidates. Sized so a\r\n * cross-axis L-route (one bend) does NOT beat the conventional\r\n * primary Z-route (two bends) on bend count alone, but DOES beat\r\n * falling back to an A* detour when the primary corridor is blocked\r\n * by an obstacle.\r\n */\r\nconst SIDE_PREFERENCE_PENALTY = 20;\r\n\r\n/**\r\n * Plausible exit/entry sides of `from` toward a partner at `toward`:\r\n * the axis-dominant side first (the classic `sideToward` choice),\r\n * then the cross-axis side facing the partner when the partner has a\r\n * material offset on that axis too. Perfectly aligned partners yield\r\n * a single candidate — offering the cross-axis side there would only\r\n * produce silly hooks.\r\n */\r\nfunction candidateSides(from: Rect, toward: Point): Side[] {\r\n const c = rectCenter(from);\r\n const dx = toward.x - c.x;\r\n const dy = toward.y - c.y;\r\n const horizontal: Side = dx >= 0 ? 'right' : 'left';\r\n const vertical: Side = dy >= 0 ? 'bottom' : 'top';\r\n if (Math.abs(dx) >= Math.abs(dy)) {\r\n return Math.abs(dy) > 1 ? [horizontal, vertical] : [horizontal];\r\n }\r\n return Math.abs(dx) > 1 ? [vertical, horizontal] : [vertical];\r\n}\r\n\r\n/**\r\n * True when any axis-aligned segment of `path` passes through the\r\n * interior of any rectangle in `obstacles`. Non-axis-aligned segments\r\n * are not expected here (the elbow router is strictly orthogonal);\r\n * diagonals are treated as intersecting so the caller falls back to A*.\r\n *\r\n * Exported for the stale-route replacement gate in MoveShapeCommand,\r\n * which compares the blocked-ness of an existing polyline against a\r\n * fresh route.\r\n */\r\nexport function pathIntersectsAny(\r\n path: readonly Point[],\r\n obstacles: readonly Rect[],\r\n): boolean {\r\n for (let i = 1; i < path.length; i += 1) {\r\n const a = path[i - 1];\r\n const b = path[i];\r\n if (a === undefined || b === undefined) return true;\r\n if (a.x === b.x) {\r\n for (const o of obstacles) {\r\n if (vSegmentIntersects(a.y, b.y, a.x, o)) return true;\r\n }\r\n } else if (a.y === b.y) {\r\n for (const o of obstacles) {\r\n if (hSegmentIntersects(a.x, b.x, a.y, o)) return true;\r\n }\r\n } else {\r\n // Diagonal — should not happen with our elbow router, but treat\r\n // as obstructed so the caller falls back to A*.\r\n return true;\r\n }\r\n }\r\n return false;\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - connection anchor distribution\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: the anchor-refinement post-pass that runs after routing\r\n * (#7780 / #7898). Two phases:\r\n *\r\n * 1. Gateway side assignment - when several flows dock on the SAME\r\n * side of a gateway, the best-aligned flow keeps that diamond tip\r\n * and the rest move to free tips (cardinal points first). This is\r\n * the classic BPMN convention (\"came from the left, yes exits\r\n * right, no exits bottom\") requested verbatim by NTT Data.\r\n * 2. Along-side spreading - remaining groups of 2+ docking points on\r\n * one side of a shape are redistributed evenly along that side\r\n * (minimum spacing, collapse to centre when the side is too\r\n * short). Port of the proven `_spread_anchors` mechanism from\r\n * Python/mindzie-bpmn/mindzie_bpmn/layout.py, which closed Bug\r\n * #7461 for AI-generated diagrams; this brings the interactive\r\n * editor to parity.\r\n *\r\n * Pure module: operates on plain shape/edge records, returns new\r\n * waypoint arrays for the edges it changed. Callers (the facade's\r\n * routing call sites) own command creation and undo.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { Point, Rect, Side } from './geometry.js';\r\nimport { rectCenter, rectInflate, coalesceCollinear } from './geometry.js';\r\nimport { computeOrthogonalRoute } from './route.js';\r\n\r\n/** Shape record: bounds plus the BPMN element type. */\r\nexport interface AnchorShapeInfo {\r\n bounds: Rect;\r\n /** BPMN element type, e.g. 'exclusiveGateway', 'task', 'startEvent'. */\r\n type: string;\r\n}\r\n\r\n/** Edge record: semantic endpoints plus the current polyline. */\r\nexport interface AnchorEdgeInfo {\r\n id: string;\r\n sourceId: string;\r\n targetId: string;\r\n waypoints: readonly Point[];\r\n}\r\n\r\n/** Minimum distance between two spread anchors on one side. */\r\nconst ANCHOR_MIN_SPACING = 12;\r\n\r\n/** Tolerance when classifying which side a waypoint sits on. */\r\nconst SIDE_EPS = 1.0;\r\n\r\nconst GATEWAY_TYPES: ReadonlySet = new Set([\r\n 'exclusiveGateway',\r\n 'parallelGateway',\r\n 'inclusiveGateway',\r\n 'eventBasedGateway',\r\n 'complexGateway',\r\n]);\r\n\r\n/** Containers never take part in anchor grouping. */\r\nconst CONTAINER_TYPES: ReadonlySet = new Set([\r\n 'participant',\r\n 'lane',\r\n 'laneSet',\r\n]);\r\n\r\n/** Stable side iteration order used for contest resolution. */\r\nconst SIDE_ORDER: readonly Side[] = ['right', 'left', 'bottom', 'top'];\r\n\r\n/** Outward unit normal per side, used to score displacement targets. */\r\nconst SIDE_NORMALS: Record = {\r\n right: { x: 1, y: 0 },\r\n left: { x: -1, y: 0 },\r\n bottom: { x: 0, y: 1 },\r\n top: { x: 0, y: -1 },\r\n};\r\n\r\ninterface EndpointEntry {\r\n edgeId: string;\r\n end: 'source' | 'target';\r\n side: Side;\r\n}\r\n\r\n/** Options for {@link refineAnchors}. */\r\nexport interface RefineAnchorsOptions {\r\n /**\r\n * Bounds of shapes that are NOT anchor-managed but may be flow\r\n * partners (used for alignment scoring and side-reassignment\r\n * re-routes). Looked up after `shapes`.\r\n */\r\n referenceBounds?: ReadonlyMap;\r\n /**\r\n * Raw (unpadded) bounds of every obstacle candidate, keyed by\r\n * element id, containers already excluded. Used by the gateway\r\n * side-reassignment re-route so displaced flows still avoid other\r\n * shapes. Endpoint shapes are excluded per edge; padding matches\r\n * the router convention (8 units).\r\n */\r\n obstacleBounds?: ReadonlyMap;\r\n}\r\n\r\n/** Router obstacle padding — mirrors obstacles.ts. */\r\nconst OBSTACLE_PADDING = 8;\r\n\r\n/**\r\n * Refines the docking anchors of `edges` on the shapes in `shapes`.\r\n *\r\n * Only shapes present in `shapes` are anchor-managed - callers scope\r\n * the pass by choosing that map (e.g. just the two endpoints of a\r\n * freshly drawn flow, or every flow node after auto-layout).\r\n *\r\n * Returns a map of edge id to new waypoints containing ONLY the edges\r\n * whose polyline actually changed. Inputs are never mutated.\r\n */\r\nexport function refineAnchors(\r\n shapes: ReadonlyMap,\r\n edges: readonly AnchorEdgeInfo[],\r\n options: RefineAnchorsOptions = {},\r\n): Map {\r\n // Working copies - all rewrites happen here. Self-loops are left\r\n // alone: their routing is a dedicated concern, not anchor spread.\r\n const working = new Map();\r\n const edgeById = new Map();\r\n for (const edge of edges) {\r\n if (edge.waypoints.length < 2) continue;\r\n if (edge.sourceId === edge.targetId) continue;\r\n working.set(\r\n edge.id,\r\n edge.waypoints.map((p) => ({ x: p.x, y: p.y })),\r\n );\r\n edgeById.set(edge.id, edge);\r\n }\r\n\r\n const boundsOf = (id: string): Rect | undefined =>\r\n shapes.get(id)?.bounds ?? options.referenceBounds?.get(id);\r\n\r\n const obstaclesFor = (edge: AnchorEdgeInfo): Rect[] => {\r\n const out: Rect[] = [];\r\n if (!options.obstacleBounds) return out;\r\n for (const [id, b] of options.obstacleBounds) {\r\n if (id === edge.sourceId || id === edge.targetId) continue;\r\n out.push(rectInflate(b, OBSTACLE_PADDING));\r\n }\r\n return out;\r\n };\r\n\r\n // Phase 1: gateway side assignment on contested sides.\r\n for (const [shapeId, info] of shapes) {\r\n if (!GATEWAY_TYPES.has(info.type)) continue;\r\n assignGatewaySides(shapeId, info.bounds, working, edgeById, boundsOf, obstaclesFor);\r\n }\r\n\r\n // Phase 2: along-side spreading for every remaining group of 2+.\r\n spreadAlongSides(shapes, working, edgeById);\r\n\r\n // Diff: only report edges whose polyline changed.\r\n const changed = new Map();\r\n for (const [id, wps] of working) {\r\n const original = edgeById.get(id);\r\n if (!original) continue;\r\n if (!waypointListsEqual(original.waypoints, wps)) {\r\n changed.set(id, wps);\r\n }\r\n }\r\n return changed;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Phase 1 - gateway side assignment */\r\n/* ------------------------------------------------------------------ */\r\n\r\nfunction assignGatewaySides(\r\n shapeId: string,\r\n bounds: Rect,\r\n working: Map,\r\n edgeById: Map,\r\n boundsOf: (id: string) => Rect | undefined,\r\n obstaclesFor: (edge: AnchorEdgeInfo) => Rect[],\r\n): void {\r\n const entries = collectEntries(shapeId, bounds, working, edgeById);\r\n if (entries.length <= 1) return;\r\n\r\n const bySide = new Map();\r\n for (const entry of entries) {\r\n const group = bySide.get(entry.side);\r\n if (group) {\r\n group.push(entry);\r\n } else {\r\n bySide.set(entry.side, [entry]);\r\n }\r\n }\r\n const occupied = new Set(bySide.keys());\r\n const center = rectCenter(bounds);\r\n\r\n for (const side of SIDE_ORDER) {\r\n const group = bySide.get(side);\r\n if (!group || group.length <= 1) continue;\r\n\r\n // The flow whose partner is best aligned with this side's axis\r\n // keeps the tip; the rest are displaced to free tips. Stable sort:\r\n // alignment first, edge id as the tiebreaker.\r\n group.sort((a, b) => {\r\n const scoreDiff =\r\n alignmentScore(a, side, center, working, edgeById, boundsOf) -\r\n alignmentScore(b, side, center, working, edgeById, boundsOf);\r\n if (scoreDiff !== 0) return scoreDiff;\r\n return a.edgeId < b.edgeId ? -1 : a.edgeId > b.edgeId ? 1 : 0;\r\n });\r\n\r\n for (let i = 1; i < group.length; i += 1) {\r\n const entry = group[i];\r\n if (entry === undefined) continue;\r\n const free = SIDE_ORDER.filter((s) => !occupied.has(s));\r\n if (free.length === 0) break; // Overflow - phase 2 spreads it.\r\n if (!displaceToSide(entry, free, center, working, edgeById, boundsOf, obstaclesFor)) {\r\n continue; // Partner bounds unknown - leave the dock in place.\r\n }\r\n occupied.add(entry.side);\r\n group.splice(i, 1);\r\n i -= 1;\r\n const moved = bySide.get(entry.side);\r\n if (moved) {\r\n moved.push(entry);\r\n } else {\r\n bySide.set(entry.side, [entry]);\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Moves `entry` to the best free gateway tip by RE-ROUTING its edge\r\n * with the new side pinned (a local waypoint rewrite could drag the\r\n * first segment back through the gateway body). The opposite end\r\n * keeps its current side when it is classifiable, so an already\r\n * settled dock does not jump. Returns false when either endpoint's\r\n * bounds are unknown; mutates `entry.side` on success.\r\n */\r\nfunction displaceToSide(\r\n entry: EndpointEntry,\r\n free: readonly Side[],\r\n center: Point,\r\n working: Map,\r\n edgeById: Map,\r\n boundsOf: (id: string) => Rect | undefined,\r\n obstaclesFor: (edge: AnchorEdgeInfo) => Rect[],\r\n): boolean {\r\n const edge = edgeById.get(entry.edgeId);\r\n if (!edge) return false;\r\n const sourceRect = boundsOf(edge.sourceId);\r\n const targetRect = boundsOf(edge.targetId);\r\n if (!sourceRect || !targetRect) return false;\r\n const wps = working.get(entry.edgeId);\r\n if (!wps || wps.length < 2) return false;\r\n\r\n const nextSide = pickDisplacementSide(\r\n free,\r\n entry,\r\n center,\r\n working,\r\n edgeById,\r\n boundsOf,\r\n );\r\n\r\n // Preserve the opposite end's current side so a settled dock stays.\r\n const first = wps[0];\r\n const last = wps[wps.length - 1];\r\n const otherSide: Side | undefined =\r\n entry.end === 'source'\r\n ? last !== undefined\r\n ? whichSide(last, targetRect)\r\n : undefined\r\n : first !== undefined\r\n ? whichSide(first, sourceRect)\r\n : undefined;\r\n\r\n const routeOptions =\r\n entry.end === 'source'\r\n ? {\r\n sourceSide: nextSide,\r\n ...(otherSide !== undefined ? { targetSide: otherSide } : {}),\r\n }\r\n : {\r\n targetSide: nextSide,\r\n ...(otherSide !== undefined ? { sourceSide: otherSide } : {}),\r\n };\r\n\r\n const next = computeOrthogonalRoute(\r\n sourceRect,\r\n targetRect,\r\n obstaclesFor(edge),\r\n routeOptions,\r\n );\r\n if (next.length < 2) return false;\r\n working.set(entry.edgeId, next);\r\n entry.side = nextSide;\r\n return true;\r\n}\r\n\r\n/**\r\n * How well an entry's partner aligns with the axis of `side` - lower\r\n * is better aligned. For left/right the score is the partner's\r\n * vertical offset from the gateway centre; for top/bottom, the\r\n * horizontal offset.\r\n */\r\nfunction alignmentScore(\r\n entry: EndpointEntry,\r\n side: Side,\r\n center: Point,\r\n working: Map,\r\n edgeById: Map,\r\n boundsOf: (id: string) => Rect | undefined,\r\n): number {\r\n const partner = partnerPoint(entry, working, edgeById, boundsOf);\r\n if (!partner) return Number.POSITIVE_INFINITY;\r\n return side === 'left' || side === 'right'\r\n ? Math.abs(partner.y - center.y)\r\n : Math.abs(partner.x - center.x);\r\n}\r\n\r\n/**\r\n * Picks the free side whose outward normal points most directly at the\r\n * entry's partner - the displacement with the smallest visual detour.\r\n */\r\nfunction pickDisplacementSide(\r\n free: readonly Side[],\r\n entry: EndpointEntry,\r\n center: Point,\r\n working: Map,\r\n edgeById: Map,\r\n boundsOf: (id: string) => Rect | undefined,\r\n): Side {\r\n const first = free[0] as Side;\r\n const partner = partnerPoint(entry, working, edgeById, boundsOf);\r\n if (!partner) return first;\r\n const dx = partner.x - center.x;\r\n const dy = partner.y - center.y;\r\n const len = Math.hypot(dx, dy);\r\n if (len === 0) return first;\r\n let best = first;\r\n let bestDot = Number.NEGATIVE_INFINITY;\r\n for (const side of free) {\r\n const n = SIDE_NORMALS[side];\r\n const dot = (n.x * dx + n.y * dy) / len;\r\n if (dot > bestDot) {\r\n bestDot = dot;\r\n best = side;\r\n }\r\n }\r\n return best;\r\n}\r\n\r\n/**\r\n * Reference point for alignment scoring: the partner shape's centre\r\n * when its bounds are known, else the waypoint adjacent to the\r\n * anchored endpoint (the approach direction).\r\n */\r\nfunction partnerPoint(\r\n entry: EndpointEntry,\r\n working: Map,\r\n edgeById: Map,\r\n boundsOf: (id: string) => Rect | undefined,\r\n): Point | undefined {\r\n const edge = edgeById.get(entry.edgeId);\r\n if (!edge) return undefined;\r\n const partnerId = entry.end === 'source' ? edge.targetId : edge.sourceId;\r\n const rect = boundsOf(partnerId);\r\n if (rect) return rectCenter(rect);\r\n const wps = working.get(entry.edgeId);\r\n if (!wps || wps.length < 2) return undefined;\r\n return entry.end === 'source' ? wps[1] : wps[wps.length - 2];\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Phase 2 - along-side spreading */\r\n/* ------------------------------------------------------------------ */\r\n\r\nfunction spreadAlongSides(\r\n shapes: ReadonlyMap,\r\n working: Map,\r\n edgeById: Map,\r\n): void {\r\n for (const [shapeId, info] of shapes) {\r\n if (CONTAINER_TYPES.has(info.type)) continue;\r\n const entries = collectEntries(shapeId, info.bounds, working, edgeById);\r\n const bySide = new Map();\r\n for (const entry of entries) {\r\n const group = bySide.get(entry.side);\r\n if (group) {\r\n group.push(entry);\r\n } else {\r\n bySide.set(entry.side, [entry]);\r\n }\r\n }\r\n for (const [side, group] of bySide) {\r\n if (group.length <= 1) continue;\r\n // Order the anchors to mirror the current visual order: sort by\r\n // the perpendicular coordinate of each entry's approach waypoint.\r\n const axis: 'x' | 'y' = side === 'left' || side === 'right' ? 'y' : 'x';\r\n group.sort((a, b) => {\r\n const diff =\r\n approachCoord(a, axis, working) - approachCoord(b, axis, working);\r\n if (diff !== 0) return diff;\r\n return a.edgeId < b.edgeId ? -1 : a.edgeId > b.edgeId ? 1 : 0;\r\n });\r\n const positions = anchorPositions(info.bounds, side, group.length);\r\n for (let i = 0; i < group.length; i += 1) {\r\n const entry = group[i];\r\n const pos = positions[i];\r\n if (entry === undefined || pos === undefined) continue;\r\n const port: Point =\r\n side === 'left'\r\n ? { x: info.bounds.x, y: pos }\r\n : side === 'right'\r\n ? { x: info.bounds.x + info.bounds.width, y: pos }\r\n : side === 'top'\r\n ? { x: pos, y: info.bounds.y }\r\n : { x: pos, y: info.bounds.y + info.bounds.height };\r\n rewriteEndpoint(working, entry.edgeId, entry.end, port, side);\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * `count` evenly spaced positions along the named side. When the side\r\n * is too short to keep `ANCHOR_MIN_SPACING` between anchors, everyone\r\n * collapses back to the centre (indistinguishable anchors are worse\r\n * than a single shared one).\r\n */\r\nexport function anchorPositions(b: Rect, side: Side, count: number): number[] {\r\n const span = side === 'left' || side === 'right' ? b.height : b.width;\r\n const origin = side === 'left' || side === 'right' ? b.y : b.x;\r\n if (count <= 1 || span / (count + 1) < ANCHOR_MIN_SPACING) {\r\n return new Array(count).fill(origin + span / 2);\r\n }\r\n const out: number[] = [];\r\n for (let i = 0; i < count; i += 1) {\r\n out.push(origin + (span * (i + 1)) / (count + 1));\r\n }\r\n return out;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Shared plumbing */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Collects the endpoints of all edges currently docked on `shapeId`. */\r\nfunction collectEntries(\r\n shapeId: string,\r\n bounds: Rect,\r\n working: Map,\r\n edgeById: Map,\r\n): EndpointEntry[] {\r\n const entries: EndpointEntry[] = [];\r\n for (const [edgeId, wps] of working) {\r\n const edge = edgeById.get(edgeId);\r\n if (!edge || wps.length < 2) continue;\r\n if (edge.sourceId === shapeId) {\r\n const wp = wps[0];\r\n const side = wp !== undefined ? whichSide(wp, bounds) : undefined;\r\n if (side !== undefined) {\r\n entries.push({ edgeId, end: 'source', side });\r\n }\r\n }\r\n if (edge.targetId === shapeId) {\r\n const wp = wps[wps.length - 1];\r\n const side = wp !== undefined ? whichSide(wp, bounds) : undefined;\r\n if (side !== undefined) {\r\n entries.push({ edgeId, end: 'target', side });\r\n }\r\n }\r\n }\r\n return entries;\r\n}\r\n\r\n/**\r\n * Which side of `b` the waypoint sits on, or undefined when it is not\r\n * on the border (e.g. a stale centre-docked endpoint) - such endpoints\r\n * are left alone rather than guessed at.\r\n */\r\nexport function whichSide(wp: Point, b: Rect): Side | undefined {\r\n if (Math.abs(wp.x - b.x) < SIDE_EPS) return 'left';\r\n if (Math.abs(wp.x - (b.x + b.width)) < SIDE_EPS) return 'right';\r\n if (Math.abs(wp.y - b.y) < SIDE_EPS) return 'top';\r\n if (Math.abs(wp.y - (b.y + b.height)) < SIDE_EPS) return 'bottom';\r\n return undefined;\r\n}\r\n\r\n/**\r\n * Perpendicular coordinate of the waypoint adjacent to the anchored\r\n * endpoint - tells us which order the lines approach from.\r\n */\r\nfunction approachCoord(\r\n entry: EndpointEntry,\r\n axis: 'x' | 'y',\r\n working: Map,\r\n): number {\r\n const wps = working.get(entry.edgeId);\r\n if (!wps || wps.length === 0) return 0;\r\n const wp =\r\n entry.end === 'source'\r\n ? (wps[1] ?? wps[0])\r\n : (wps[wps.length - 2] ?? wps[wps.length - 1]);\r\n if (wp === undefined) return 0;\r\n return axis === 'y' ? wp.y : wp.x;\r\n}\r\n\r\n/**\r\n * Moves one endpoint of an edge to `port` on side `side` and adjusts\r\n * the adjacent waypoint so the polyline stays orthogonal. Two-point\r\n * edges gain a Z-bend at the midpoint (port of `_set_exit_waypoint` /\r\n * `_set_entry_waypoint` from mindzie-bpmn layout.py).\r\n */\r\nfunction rewriteEndpoint(\r\n working: Map,\r\n edgeId: string,\r\n end: 'source' | 'target',\r\n port: Point,\r\n side: Side,\r\n): void {\r\n const wps = working.get(edgeId);\r\n if (!wps || wps.length < 2) return;\r\n const next = rewriteEndpointTo(wps, end, port, side);\r\n if (next !== null) {\r\n working.set(edgeId, next);\r\n }\r\n}\r\n\r\n/**\r\n * Pure endpoint rewrite: returns a fresh polyline with `end` moved to\r\n * `port` on side `side` and the adjacent waypoint aligned so the\r\n * polyline stays orthogonal. Two-point edges gain a Z-bend. Returns\r\n * null when the input is too short to rewrite. Also used by the\r\n * facade's same-shape endpoint re-dock (#7902).\r\n */\r\nexport function rewriteEndpointTo(\r\n waypoints: readonly Point[],\r\n end: 'source' | 'target',\r\n port: Point,\r\n side: Side,\r\n): Point[] | null {\r\n if (waypoints.length < 2) return null;\r\n const horizontal = side === 'left' || side === 'right';\r\n\r\n if (waypoints.length === 2) {\r\n if (end === 'source') {\r\n const t = waypoints[1];\r\n if (t === undefined) return null;\r\n if (horizontal) {\r\n const midX = (port.x + t.x) / 2;\r\n return coalesceCollinear([\r\n { x: port.x, y: port.y },\r\n { x: midX, y: port.y },\r\n { x: midX, y: t.y },\r\n { x: t.x, y: t.y },\r\n ]);\r\n }\r\n const midY = (port.y + t.y) / 2;\r\n return coalesceCollinear([\r\n { x: port.x, y: port.y },\r\n { x: port.x, y: midY },\r\n { x: t.x, y: midY },\r\n { x: t.x, y: t.y },\r\n ]);\r\n }\r\n const s = waypoints[0];\r\n if (s === undefined) return null;\r\n if (horizontal) {\r\n const midX = (s.x + port.x) / 2;\r\n return coalesceCollinear([\r\n { x: s.x, y: s.y },\r\n { x: midX, y: s.y },\r\n { x: midX, y: port.y },\r\n { x: port.x, y: port.y },\r\n ]);\r\n }\r\n const midY = (s.y + port.y) / 2;\r\n return coalesceCollinear([\r\n { x: s.x, y: s.y },\r\n { x: s.x, y: midY },\r\n { x: port.x, y: midY },\r\n { x: port.x, y: port.y },\r\n ]);\r\n }\r\n\r\n // 3+ waypoints: the segment at the anchor is perpendicular to the\r\n // side - keep it that way by aligning the neighbour on the free axis.\r\n const next = waypoints.map((p) => ({ x: p.x, y: p.y }));\r\n if (end === 'source') {\r\n const neighbor = next[1];\r\n if (neighbor === undefined) return null;\r\n next[1] = horizontal\r\n ? { x: neighbor.x, y: port.y }\r\n : { x: port.x, y: neighbor.y };\r\n next[0] = { x: port.x, y: port.y };\r\n } else {\r\n const neighbor = next[next.length - 2];\r\n if (neighbor === undefined) return null;\r\n next[next.length - 2] = horizontal\r\n ? { x: neighbor.x, y: port.y }\r\n : { x: port.x, y: neighbor.y };\r\n next[next.length - 1] = { x: port.x, y: port.y };\r\n }\r\n return coalesceCollinear(next);\r\n}\r\n\r\nfunction waypointListsEqual(\r\n a: readonly Point[],\r\n b: readonly Point[],\r\n): boolean {\r\n if (a.length !== b.length) return false;\r\n for (let i = 0; i < a.length; i += 1) {\r\n const pa = a[i];\r\n const pb = b[i];\r\n if (pa === undefined || pb === undefined) return false;\r\n if (pa.x !== pb.x || pa.y !== pb.y) return false;\r\n }\r\n return true;\r\n}\r\n","/**\r\n * mindzie.BpmnEditor — command stack + concrete commands\r\n * Copyright (c) mindzie 2026 — proprietary, all rights reserved\r\n *\r\n * Implements: an undoable command stack and the first concrete\r\n * command type (`SetNameCommand`). Commands are plain objects with\r\n * `execute(doc)` / `undo(doc)` methods; they hold the delta (before +\r\n * after values) and receive the document at apply time. The stack\r\n * tracks undo and redo positions; pushing a new command clears the\r\n * redo stack. Emits a `command-executed` event on every push / undo /\r\n * redo for the facade to bridge to the public event bus.\r\n *\r\n * Per IMPLEMENTATION_SPEC.md §17.2 and docs/PHASE_5_PLAN.md.\r\n *\r\n * Design influences: original — classic \"memento\" pattern applied to\r\n * our BPMN model. Commands are pure data + two methods; the stack has\r\n * no knowledge of command internals.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type {\r\n BpmnDocument,\r\n BpmnElement,\r\n EventDefinition,\r\n LoopCharacteristics,\r\n} from '../model/index.js';\r\nimport { withLabelPlacementPin } from '../model/label-pin.js';\r\nimport { uniqueElementId } from '../model/unique-id.js';\r\nimport {\r\n collectDataAssociationEnds,\r\n collectDataAssociationTouches,\r\n resolveEdgeEnds,\r\n type EdgeEnds,\r\n} from '../model/data-association-ends.js';\r\nimport type { Point, Rect, Side } from '../view/routing/geometry.js';\r\nimport {\r\n coalesceCollinear,\r\n dockSide,\r\n rectCenter,\r\n routeCost,\r\n sideFacesPoint,\r\n sidePort,\r\n sideToward,\r\n} from '../view/routing/geometry.js';\r\nimport {\r\n computeOrthogonalRoute,\r\n pathIntersectsAny,\r\n} from '../view/routing/route.js';\r\nimport {\r\n refineAnchors,\r\n type AnchorShapeInfo,\r\n type AnchorEdgeInfo,\r\n} from '../view/routing/anchors.js';\r\n\r\n/**\r\n * A re-anchored polyline whose cost (Manhattan length + bend\r\n * penalties) exceeds the fresh route's cost by this factor is treated\r\n * as drag-history residue and replaced. Deliberate author detours\r\n * measure well under 2x the optimal route; the backwards loops and\r\n * doubled-back corridors left by accumulated drags measure well over.\r\n */\r\nconst STALE_ROUTE_COST_FACTOR = 2;\r\n\r\n/**\r\n * Lower staleness bar applied when a flow docks on the side of a\r\n * shape facing AWAY from its partner (e.g. entering the target from\r\n * the right while the source sits far to the left) — the signature\r\n * of a backwards-routed edge. Wrong-side routes that are only\r\n * modestly more expensive than the fresh route are still garbage;\r\n * the small margin merely avoids replacing routes the fresh router\r\n * itself would reproduce. Note the anchor-distribution pass (#7898)\r\n * runs AFTER this gate, so gateway flows deliberately spread onto\r\n * distinct diamond tips are re-pinned even if the gate straightens\r\n * them first.\r\n */\r\nconst WRONG_SIDE_COST_FACTOR = 1.2;\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Command contract */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * A single reversible mutation of a `BpmnDocument`. Implementations\r\n * hold the delta (before/after values or inserted/removed payloads)\r\n * and receive the document at `execute`/`undo` time so they remain\r\n * shareable between documents that happen to have compatible shapes\r\n * (in practice, they only ever see their owning document).\r\n */\r\nexport interface Command {\r\n /** Stable machine-readable kind, for telemetry and test assertions. */\r\n readonly kind: string;\r\n /** Human-readable description (\"Rename task to 'Approve order'\"). */\r\n readonly description: string;\r\n /** Apply the change. */\r\n execute(doc: BpmnDocument): void;\r\n /** Reverse the change. Must restore the exact prior state. */\r\n undo(doc: BpmnDocument): void;\r\n}\r\n\r\n/**\r\n * Event emitted by the stack after a command is executed, undone, or\r\n * redone. The facade bridges this to the public event bus.\r\n */\r\nexport interface CommandExecutedEvent {\r\n command: Command;\r\n direction: 'execute' | 'undo' | 'redo';\r\n stackDepth: number;\r\n canUndo: boolean;\r\n canRedo: boolean;\r\n}\r\n\r\n/** Function signature for subscribers. */\r\nexport type CommandListener = (ev: CommandExecutedEvent) => void;\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Stack */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Maintains the undo/redo history for a single mounted editor instance.\r\n * Construct once per render; dispose when the render is torn down.\r\n */\r\nexport class CommandStack {\r\n private readonly undoStack: Command[] = [];\r\n private readonly redoStack: Command[] = [];\r\n private readonly listeners: Set = new Set();\r\n private activeBatch:\r\n | {\r\n description: string;\r\n commands: Command[];\r\n }\r\n | null = null;\r\n\r\n /**\r\n * Execute a command and push it onto the undo stack. Pushing clears\r\n * the redo stack — once you branch, the forward history is gone.\r\n */\r\n public push(command: Command, doc: BpmnDocument): void {\r\n command.execute(doc);\r\n if (this.activeBatch) {\r\n this.activeBatch.commands.push(command);\r\n return;\r\n }\r\n this.undoStack.push(command);\r\n this.redoStack.length = 0;\r\n this.emit(command, 'execute');\r\n }\r\n\r\n /**\r\n * Capture commands executed by `work` and store them as one undo\r\n * step. The individual commands still execute immediately; only the\r\n * history entry is bundled.\r\n */\r\n public batch(description: string, work: () => T): T {\r\n if (this.activeBatch) {\r\n return work();\r\n }\r\n\r\n this.activeBatch = { description, commands: [] };\r\n try {\r\n const result = work();\r\n const commands = this.activeBatch.commands.slice();\r\n if (commands.length > 0) {\r\n const command =\r\n commands.length === 1\r\n ? commands[0]!\r\n : new CompositeCommand(commands, description);\r\n this.undoStack.push(command);\r\n this.redoStack.length = 0;\r\n this.emit(command, 'execute');\r\n }\r\n return result;\r\n } finally {\r\n this.activeBatch = null;\r\n }\r\n }\r\n\r\n /**\r\n * Pop the most recent command and call its `undo`. No-op when the\r\n * undo stack is empty.\r\n */\r\n public undo(doc: BpmnDocument): boolean {\r\n const command = this.undoStack.pop();\r\n if (!command) {\r\n return false;\r\n }\r\n command.undo(doc);\r\n this.redoStack.push(command);\r\n this.emit(command, 'undo');\r\n return true;\r\n }\r\n\r\n /**\r\n * Pop the most recent redo candidate and call its `execute`. No-op\r\n * when the redo stack is empty.\r\n */\r\n public redo(doc: BpmnDocument): boolean {\r\n const command = this.redoStack.pop();\r\n if (!command) {\r\n return false;\r\n }\r\n command.execute(doc);\r\n this.undoStack.push(command);\r\n this.emit(command, 'redo');\r\n return true;\r\n }\r\n\r\n /** True when a command is available to undo. */\r\n public canUndo(): boolean {\r\n return this.undoStack.length > 0;\r\n }\r\n\r\n /** True when a command is available to redo. */\r\n public canRedo(): boolean {\r\n return this.redoStack.length > 0;\r\n }\r\n\r\n /** Number of commands on the undo stack. */\r\n public depth(): number {\r\n return this.undoStack.length;\r\n }\r\n\r\n /** Clear both stacks and subscribers. */\r\n public dispose(): void {\r\n this.undoStack.length = 0;\r\n this.redoStack.length = 0;\r\n this.listeners.clear();\r\n }\r\n\r\n /** Clear both stacks without affecting subscribers. */\r\n public clear(): void {\r\n this.undoStack.length = 0;\r\n this.redoStack.length = 0;\r\n }\r\n\r\n /**\r\n * Subscribe to command events. Returns an unsubscribe function.\r\n */\r\n public onChange(listener: CommandListener): () => void {\r\n this.listeners.add(listener);\r\n return () => {\r\n this.listeners.delete(listener);\r\n };\r\n }\r\n\r\n /* ------------- Internals ------------- */\r\n\r\n private emit(command: Command, direction: CommandExecutedEvent['direction']): void {\r\n const ev: CommandExecutedEvent = {\r\n command,\r\n direction,\r\n stackDepth: this.undoStack.length,\r\n canUndo: this.canUndo(),\r\n canRedo: this.canRedo(),\r\n };\r\n const snapshot = Array.from(this.listeners);\r\n for (const listener of snapshot) {\r\n listener(ev);\r\n }\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* First concrete command: SetName */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Changes the `name` property on a single element. Works on any\r\n * BPMN element whose model shape includes an optional string `name`\r\n * (tasks, events, gateways, subprocesses, data objects, participants,\r\n * lanes, flows — effectively every labelled element).\r\n */\r\nexport class SetNameCommand implements Command {\r\n public readonly kind: string = 'setName';\r\n public readonly description: string;\r\n private readonly elementId: string;\r\n private readonly previousName: string | undefined;\r\n private readonly nextName: string | undefined;\r\n\r\n public constructor(elementId: string, previousName: string | undefined, nextName: string | undefined) {\r\n this.elementId = elementId;\r\n this.previousName = previousName;\r\n this.nextName = nextName;\r\n const before = previousName ?? '(unnamed)';\r\n const after = nextName ?? '(unnamed)';\r\n this.description = `Rename ${elementId}: \"${before}\" -> \"${after}\"`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n const element = doc.byId.get(this.elementId);\r\n if (!element) {\r\n return;\r\n }\r\n applyName(element, this.nextName);\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n const element = doc.byId.get(this.elementId);\r\n if (!element) {\r\n return;\r\n }\r\n applyName(element, this.previousName);\r\n }\r\n}\r\n\r\nfunction applyName(element: BpmnElement, value: string | undefined): void {\r\n const target = element as BpmnElement & { name?: string };\r\n if (value === undefined || value.length === 0) {\r\n delete target.name;\r\n } else {\r\n target.name = value;\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Second concrete command: DeleteElement */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Remembered position for an object inside a parent array. */\r\ninterface ArraySlot {\r\n parent: BpmnElement[];\r\n index: number;\r\n element: BpmnElement;\r\n}\r\n\r\n/**\r\n * Remove an element (and cascade-remove every sequence/message flow\r\n * that references it as source/target). Tracks all removed slots so\r\n * undo can restore at exact positions.\r\n *\r\n * Scope α: supports shapes and edges that live in `flowElements`,\r\n * `planeElements`, or as `collaboration.messageFlows`. Elements with\r\n * child content (subprocess.flowElements) are deleted along with\r\n * their children since they disappear from `byId` and won't be reached.\r\n */\r\nexport class DeleteElementCommand implements Command {\r\n public readonly kind: string = 'deleteElement';\r\n public readonly description: string;\r\n private readonly elementId: string;\r\n private readonly slots: ArraySlot[] = [];\r\n private readonly diSlots: ArraySlot[] = [];\r\n private readonly byIdEntries: { id: string; element: BpmnElement }[] = [];\r\n private captured: boolean = false;\r\n\r\n public constructor(elementId: string) {\r\n this.elementId = elementId;\r\n this.description = `Delete element ${elementId}`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n const element = doc.byId.get(this.elementId);\r\n if (!element) {\r\n return;\r\n }\r\n\r\n if (!this.captured) {\r\n this.captureSlots(doc, element);\r\n this.captured = true;\r\n }\r\n\r\n for (const slot of this.slots) {\r\n const idx = slot.parent.indexOf(slot.element);\r\n if (idx >= 0) {\r\n slot.parent.splice(idx, 1);\r\n }\r\n }\r\n for (const slot of this.diSlots) {\r\n const idx = slot.parent.indexOf(slot.element);\r\n if (idx >= 0) {\r\n slot.parent.splice(idx, 1);\r\n }\r\n }\r\n for (const entry of this.byIdEntries) {\r\n doc.byId.delete(entry.id);\r\n }\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n // Restore byId first so references resolve during validation.\r\n for (const entry of this.byIdEntries) {\r\n doc.byId.set(entry.id, entry.element);\r\n }\r\n // Splice back at the remembered indices (sorted ascending so later\r\n // insertions don't shift earlier ones). Slots were captured in\r\n // original order so this is a reverse of the removals.\r\n for (let i = this.slots.length - 1; i >= 0; i -= 1) {\r\n const slot = this.slots[i];\r\n if (!slot) continue;\r\n const clamped = Math.min(slot.index, slot.parent.length);\r\n slot.parent.splice(clamped, 0, slot.element);\r\n }\r\n for (let i = this.diSlots.length - 1; i >= 0; i -= 1) {\r\n const slot = this.diSlots[i];\r\n if (!slot) continue;\r\n const clamped = Math.min(slot.index, slot.parent.length);\r\n slot.parent.splice(clamped, 0, slot.element);\r\n }\r\n }\r\n\r\n private captureSlots(doc: BpmnDocument, element: BpmnElement): void {\r\n // IDs we are removing — the element itself plus any flow whose\r\n // source/target references it (cascade).\r\n const cascadeIds = new Set([this.elementId]);\r\n for (const [id, candidate] of doc.byId) {\r\n if (candidate === element) continue;\r\n const typed = candidate as BpmnElement & { sourceRef?: string; targetRef?: string };\r\n if (typed.sourceRef === this.elementId || typed.targetRef === this.elementId) {\r\n cascadeIds.add(id);\r\n }\r\n }\r\n // Data associations go with their owner and with any data item they\r\n // name. Their sourceRef is a list and their owner end is implied by\r\n // nesting, so the scalar check above never matches them (#9580).\r\n for (const [id, touched] of collectDataAssociationTouches(doc)) {\r\n if (touched.has(this.elementId)) {\r\n cascadeIds.add(id);\r\n }\r\n }\r\n\r\n // Walk the document to find container arrays; remember the exact\r\n // slot each cascaded element lives in.\r\n walkArrays(doc, (parent) => {\r\n for (let i = 0; i < parent.length; i += 1) {\r\n const candidate = parent[i];\r\n if (!candidate) continue;\r\n if (cascadeIds.has(candidate.id)) {\r\n this.slots.push({ parent, index: i, element: candidate });\r\n }\r\n const diRef = (candidate as BpmnElement & { bpmnElement?: string }).bpmnElement;\r\n if (\r\n typeof diRef === 'string' &&\r\n cascadeIds.has(diRef) &&\r\n (candidate.type === 'BPMNShape' || candidate.type === 'BPMNEdge')\r\n ) {\r\n this.diSlots.push({ parent, index: i, element: candidate });\r\n }\r\n }\r\n });\r\n\r\n // Capture byId entries for rollback.\r\n for (const id of cascadeIds) {\r\n const el = doc.byId.get(id);\r\n if (el) {\r\n this.byIdEntries.push({ id, element: el });\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Walks every array-of-BpmnElement we might need to splice into —\r\n * process.flowElements recursively through subProcess, collaboration\r\n * messageFlows + artifacts, participants, lane.flowNodeRefs (tracked\r\n * for completeness though they're strings not elements),\r\n * BPMNDiagram.plane.planeElements.\r\n */\r\nfunction walkArrays(doc: BpmnDocument, visit: (parent: BpmnElement[]) => void): void {\r\n for (const root of doc.rootElements) {\r\n visitContainer(root, visit);\r\n }\r\n for (const diagram of doc.diagrams) {\r\n visitContainer(diagram, visit);\r\n }\r\n}\r\n\r\nfunction visitContainer(element: BpmnElement, visit: (parent: BpmnElement[]) => void): void {\r\n const keys: readonly string[] = [\r\n 'flowElements',\r\n 'planeElements',\r\n 'messageFlows',\r\n 'artifacts',\r\n 'participants',\r\n 'laneSets',\r\n 'lanes',\r\n 'childLaneSets',\r\n // Nested inside the activity or event that owns them (#9580).\r\n 'dataInputAssociations',\r\n 'dataOutputAssociations',\r\n ];\r\n for (const key of keys) {\r\n const maybe = (element as unknown as Record)[key];\r\n if (Array.isArray(maybe)) {\r\n const parent = maybe as BpmnElement[];\r\n visit(parent);\r\n for (const child of parent) {\r\n if (child && typeof child === 'object') {\r\n visitContainer(child, visit);\r\n }\r\n }\r\n }\r\n }\r\n // Diagram -> plane -> planeElements chain.\r\n const plane = (element as BpmnElement & { plane?: BpmnElement }).plane;\r\n if (plane) {\r\n visitContainer(plane, visit);\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Third concrete command: MoveShape */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Shift a single shape's DI bounds by (dx, dy), and shift the\r\n * connecting endpoints of every edge attached to it so connections\r\n * stay visually attached. Non-endpoint waypoints are left alone — we\r\n * preserve the author's routing geometry.\r\n *\r\n * Attached-edge policy: on execute, snapshot every attached edge's\r\n * waypoints; then re-anchor each attached endpoint to the correct side\r\n * port of the shape's new bounds, inserting one orthogonal elbow when\r\n * the first / last segment would otherwise be diagonal. Interior\r\n * waypoints (author-placed bends) are preserved. Undo restores the\r\n * captured snapshots byte-for-byte.\r\n */\r\n/**\r\n * Returns the ids of every boundary event attached to `hostId`. Boundary\r\n * events are docked on a host activity's perimeter (`attachedToRef`), so\r\n * host geometry changes (move / resize) must carry them along.\r\n */\r\nfunction collectAttachedBoundaryIds(doc: BpmnDocument, hostId: string): Set {\r\n const ids = new Set();\r\n for (const [id, el] of doc.byId) {\r\n if (\r\n el.type === 'boundaryEvent' &&\r\n (el as BpmnElement & { attachedToRef?: string }).attachedToRef === hostId\r\n ) {\r\n ids.add(id);\r\n }\r\n }\r\n return ids;\r\n}\r\n\r\n/**\r\n * True when a flow endpoint reference names one of the shapes a move is\r\n * relocating. `sourceRef` / `targetRef` are optional on the parsed\r\n * element, so the undefined case is answered here once rather than at\r\n * each call site.\r\n */\r\nfunction isMovedEnd(moved: ReadonlySet, ref: string | undefined): boolean {\r\n return ref !== undefined && moved.has(ref);\r\n}\r\n\r\n/** The post-move bounds of a moved endpoint, or undefined if it has no shape. */\r\nfunction movedEndBounds(\r\n shapeBoundsById: ReadonlyMap,\r\n ref: string | undefined,\r\n): Rect | undefined {\r\n return ref === undefined ? undefined : shapeBoundsById.get(ref);\r\n}\r\n\r\n/**\r\n * Re-anchors the boundary events attached to `hostId` when the host's\r\n * bounding box changes from `before` to `after`. Each boundary event's\r\n * CENTER keeps the same fractional position (u, v) within the host box,\r\n * so a pure translation slides them by the same delta and a resize keeps\r\n * them proportionally on the same edge. Imported diagrams stay untouched\r\n * until their host is actually edited (this only runs inside move/resize\r\n * commands).\r\n */\r\nfunction reanchorBoundaryEvents(\r\n doc: BpmnDocument,\r\n hostId: string,\r\n before: { x: number; y: number; width: number; height: number },\r\n after: { x: number; y: number; width: number; height: number },\r\n): void {\r\n const attached = collectAttachedBoundaryIds(doc, hostId);\r\n if (attached.size === 0) return;\r\n const bw = before.width || 1;\r\n const bh = before.height || 1;\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNShape') return;\r\n const shape = pe as BpmnElement & {\r\n bpmnElement: string;\r\n bounds?: { x: number; y: number; width: number; height: number };\r\n label?: { bounds?: { x: number; y: number; width: number; height: number } };\r\n };\r\n if (!attached.has(shape.bpmnElement) || !shape.bounds) return;\r\n const cx = shape.bounds.x + shape.bounds.width / 2;\r\n const cy = shape.bounds.y + shape.bounds.height / 2;\r\n const u = (cx - before.x) / bw;\r\n const v = (cy - before.y) / bh;\r\n const ddx = after.x + u * after.width - cx;\r\n const ddy = after.y + v * after.height - cy;\r\n shape.bounds.x += ddx;\r\n shape.bounds.y += ddy;\r\n if (shape.label?.bounds) {\r\n shape.label.bounds.x += ddx;\r\n shape.label.bounds.y += ddy;\r\n }\r\n });\r\n}\r\n\r\nexport class MoveShapeCommand implements Command {\r\n public readonly kind: string = 'moveShape';\r\n public readonly description: string;\r\n private readonly elementId: string;\r\n private readonly dx: number;\r\n private readonly dy: number;\r\n // Populated on first execute; reused on redo (state at redo-time is\r\n // identical to the state at the first execute) and consumed by undo.\r\n private edgeWaypointSnapshots: Map | null = null;\r\n // Data association ends, resolved ONCE before the first execute moves\r\n // anything. A multi-source input association picks its visible source\r\n // by geometry, so resolving again after the move could pick a different\r\n // source than the one snapshotted for undo (#9580).\r\n private dataEnds: Map | null = null;\r\n\r\n public constructor(elementId: string, dx: number, dy: number) {\r\n this.elementId = elementId;\r\n this.dx = dx;\r\n this.dy = dy;\r\n this.description = `Move ${elementId} by (${String(dx)}, ${String(dy)})`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n this.pinnedDataEnds(doc);\r\n if (this.edgeWaypointSnapshots === null) {\r\n this.edgeWaypointSnapshots = this.snapshotAttachedEdges(doc);\r\n }\r\n this.shiftShapeBounds(doc, this.dx, this.dy);\r\n this.reanchorAttachedEdges(doc);\r\n }\r\n\r\n private pinnedDataEnds(doc: BpmnDocument): Map {\r\n if (this.dataEnds === null) {\r\n this.dataEnds = collectDataAssociationEnds(doc);\r\n }\r\n return this.dataEnds;\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n this.shiftShapeBounds(doc, -this.dx, -this.dy);\r\n if (this.edgeWaypointSnapshots !== null) {\r\n this.restoreEdgeWaypoints(doc, this.edgeWaypointSnapshots);\r\n }\r\n }\r\n\r\n /**\r\n * Every shape this command relocates: the moved element itself plus\r\n * the boundary events docked on it. They travel as one body, so edge\r\n * re-anchoring has to count all of them as moved - a carried boundary\r\n * event whose own escape flow stays behind still leaves the picture\r\n * disagreeing with the model, and the disagreement survives export\r\n * (#8227).\r\n *\r\n * For a host with nothing docked on it this is the single moved id,\r\n * so every other move keeps exactly its established behaviour.\r\n */\r\n private movedShapeIds(doc: BpmnDocument): Set {\r\n const moved = collectAttachedBoundaryIds(doc, this.elementId);\r\n moved.add(this.elementId);\r\n return moved;\r\n }\r\n\r\n private shiftShapeBounds(doc: BpmnDocument, dx: number, dy: number): void {\r\n if (dx === 0 && dy === 0) return;\r\n // Boundary events docked on this host translate with it so they stay\r\n // on its edge (mirrors how attached edges follow the move).\r\n const moved = this.movedShapeIds(doc);\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNShape') return;\r\n const shape = pe as BpmnElement & {\r\n bpmnElement: string;\r\n bounds?: { x: number; y: number; width: number; height: number };\r\n label?: { bounds?: { x: number; y: number; width: number; height: number } };\r\n };\r\n if (!moved.has(shape.bpmnElement) || !shape.bounds) return;\r\n shape.bounds.x += dx;\r\n shape.bounds.y += dy;\r\n if (shape.label?.bounds) {\r\n shape.label.bounds.x += dx;\r\n shape.label.bounds.y += dy;\r\n }\r\n });\r\n }\r\n\r\n private snapshotAttachedEdges(doc: BpmnDocument): Map {\r\n const byId = doc.byId;\r\n const moved = this.movedShapeIds(doc);\r\n const dataEnds = this.pinnedDataEnds(doc);\r\n const snap = new Map();\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNEdge') return;\r\n const edge = pe as BpmnElement & {\r\n bpmnElement: string;\r\n waypoints?: Point[];\r\n };\r\n if (!edge.waypoints || edge.waypoints.length < 2) return;\r\n const flow = byId.get(edge.bpmnElement);\r\n if (!flow) return;\r\n const ends = resolveEdgeEnds(flow, dataEnds);\r\n if (!isMovedEnd(moved, ends.sourceRef) && !isMovedEnd(moved, ends.targetRef)) {\r\n return;\r\n }\r\n // Deep-clone each waypoint so later mutations do not alias back\r\n // into the snapshot.\r\n snap.set(\r\n edge.id,\r\n edge.waypoints.map((p) => ({ x: p.x, y: p.y })),\r\n );\r\n });\r\n return snap;\r\n }\r\n\r\n private reanchorAttachedEdges(doc: BpmnDocument): void {\r\n // Index every shape by bpmnElement id so we can look up the\r\n // *other* end of an attached edge without a second plane walk per\r\n // edge. We also need this to run the post-reanchor simplification\r\n // pass, which routes between the two shapes' bounds.\r\n const moved = this.movedShapeIds(doc);\r\n const shapeBoundsById = new Map();\r\n // The moved shape's new bounds are captured during the walk.\r\n let newBounds: Rect | undefined;\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNShape') return;\r\n const shape = pe as BpmnElement & {\r\n bpmnElement: string;\r\n bounds?: Rect;\r\n };\r\n if (!shape.bpmnElement || !shape.bounds) return;\r\n shapeBoundsById.set(shape.bpmnElement, shape.bounds);\r\n if (shape.bpmnElement === this.elementId) {\r\n newBounds = shape.bounds;\r\n }\r\n });\r\n if (!newBounds) return;\r\n const bounds = newBounds;\r\n\r\n // Obstacle list for the simplification router: every shape except\r\n // the two endpoints of the edge being routed. Pool / lane\r\n // containers would still count as obstacles here, which is\r\n // conservative; if a pool is between the endpoints the router\r\n // correctly avoids the \"simpler\" route. Excluded per-edge below.\r\n const allObstacleBounds: Rect[] = [];\r\n for (const r of shapeBoundsById.values()) {\r\n allObstacleBounds.push(r);\r\n }\r\n\r\n const byId = doc.byId;\r\n const dataEnds = this.pinnedDataEnds(doc);\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNEdge') return;\r\n const edge = pe as BpmnElement & {\r\n bpmnElement: string;\r\n waypoints?: Point[];\r\n };\r\n if (!edge.waypoints || edge.waypoints.length < 2) return;\r\n const flow = byId.get(edge.bpmnElement);\r\n if (!flow) return;\r\n // A data association names only one of its ends; the other is the\r\n // activity it is nested in (#9580).\r\n const ends = resolveEdgeEnds(flow, dataEnds);\r\n const isSource = isMovedEnd(moved, ends.sourceRef);\r\n const isTarget = isMovedEnd(moved, ends.targetRef);\r\n if (!isSource && !isTarget) return;\r\n\r\n // Use the *other* shape's centroid as the side-selection reference\r\n // so reanchorEnd picks the side facing the other shape, not the\r\n // side facing some stale interior bend.\r\n const otherRef = isSource ? ends.targetRef : ends.sourceRef;\r\n const otherRect =\r\n otherRef !== undefined ? shapeBoundsById.get(otherRef) : undefined;\r\n const sideRef: Point | undefined = otherRect\r\n ? {\r\n x: otherRect.x + otherRect.width / 2,\r\n y: otherRect.y + otherRect.height / 2,\r\n }\r\n : undefined;\r\n // Each moved end is re-anchored against ITS OWN new bounds. For the\r\n // moved element that is `bounds`; for a boundary event carried on\r\n // its border it is the event's own box, which is what keeps its\r\n // escape flow docked on the event rather than on the host (#8227).\r\n let wps: Point[] = edge.waypoints;\r\n if (isSource) {\r\n const rect = movedEndBounds(shapeBoundsById, ends.sourceRef) ?? bounds;\r\n wps = reanchorEnd(wps, rect, 'source', sideRef);\r\n }\r\n if (isTarget) {\r\n const rect = movedEndBounds(shapeBoundsById, ends.targetRef) ?? bounds;\r\n wps = reanchorEnd(wps, rect, 'target', sideRef);\r\n }\r\n\r\n // Simplification pass: when a shape has been dragged across\r\n // many moves, the re-anchor above only rewrites the first /\r\n // last leg. Interior bends from previous moves accumulate and\r\n // can form spaghetti detours. The re-anchored polyline is\r\n // compared against the natural orthogonal route and replaced\r\n // when it is stale:\r\n // - the fresh route needs fewer waypoints, or\r\n // - the current polyline cuts through a shape the fresh route\r\n // avoids, or\r\n // - the current polyline costs (length + bends) more than\r\n // STALE_ROUTE_COST_FACTOR times the fresh route — the\r\n // signature of a backwards loop left behind by drag history.\r\n // Moderate author detours (under the factor, obstacle-free)\r\n // are preserved verbatim so deliberate bends are not thrown\r\n // away gratuitously, and a blocked fresh route never replaces\r\n // an unblocked one.\r\n const sourceRef = ends.sourceRef;\r\n const targetRef = ends.targetRef;\r\n if (sourceRef !== undefined && targetRef !== undefined) {\r\n const sourceRect = shapeBoundsById.get(sourceRef);\r\n const targetRect = shapeBoundsById.get(targetRef);\r\n if (sourceRect && targetRect && sourceRect !== targetRect) {\r\n const obstacles = allObstacleBounds.filter(\r\n (r) => r !== sourceRect && r !== targetRect,\r\n );\r\n const fresh = computeOrthogonalRoute(\r\n sourceRect,\r\n targetRect,\r\n obstacles,\r\n );\r\n if (fresh.length > 0) {\r\n const freshBlocked = pathIntersectsAny(fresh, obstacles);\r\n const currentBlocked = pathIntersectsAny(wps, obstacles);\r\n const acceptable = !freshBlocked || currentBlocked;\r\n const costFactor = hasWrongSideDock(wps, sourceRect, targetRect)\r\n ? WRONG_SIDE_COST_FACTOR\r\n : STALE_ROUTE_COST_FACTOR;\r\n const stale =\r\n fresh.length < wps.length ||\r\n (currentBlocked && !freshBlocked) ||\r\n routeCost(wps) > routeCost(fresh) * costFactor;\r\n if (acceptable && stale) {\r\n wps = fresh;\r\n }\r\n }\r\n }\r\n }\r\n\r\n edge.waypoints = wps;\r\n });\r\n\r\n // Anchor distribution (#7780 / #7898): the re-anchor above docks\r\n // every attached edge on a side midpoint, so moving a gateway (or\r\n // any shape with several flows) stacks its lines on one point.\r\n // Redistribute docks on the moved shape. Only attached edges are\r\n // rewritten, and those are exactly the ones snapshotted for undo.\r\n this.redistributeAnchors(doc, shapeBoundsById, bounds);\r\n }\r\n\r\n private redistributeAnchors(\r\n doc: BpmnDocument,\r\n shapeBoundsById: Map,\r\n movedBounds: Rect,\r\n ): void {\r\n const movedType = doc.byId.get(this.elementId)?.type;\r\n if (movedType === undefined) return;\r\n const anchorShapes = new Map([\r\n [this.elementId, { bounds: movedBounds, type: movedType }],\r\n ]);\r\n const obstacleBounds = new Map();\r\n for (const [id, b] of shapeBoundsById) {\r\n const t = doc.byId.get(id)?.type;\r\n if (t === undefined) continue;\r\n if (t === 'participant' || t === 'lane' || t === 'laneSet') continue;\r\n obstacleBounds.set(id, b);\r\n }\r\n\r\n const byId = doc.byId;\r\n const edgeInfos: AnchorEdgeInfo[] = [];\r\n const diEdgeByFlow = new Map();\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNEdge') return;\r\n const edge = pe as BpmnElement & {\r\n bpmnElement: string;\r\n waypoints?: Point[];\r\n };\r\n if (!edge.waypoints || edge.waypoints.length < 2) return;\r\n const flow = byId.get(edge.bpmnElement) as\r\n | (BpmnElement & { sourceRef?: string; targetRef?: string })\r\n | undefined;\r\n if (!flow) return;\r\n if (flow.type !== 'sequenceFlow' && flow.type !== 'messageFlow') return;\r\n const sourceRef = flow.sourceRef;\r\n const targetRef = flow.targetRef;\r\n if (sourceRef === undefined || targetRef === undefined) return;\r\n if (sourceRef !== this.elementId && targetRef !== this.elementId) return;\r\n edgeInfos.push({\r\n id: edge.bpmnElement,\r\n sourceId: sourceRef,\r\n targetId: targetRef,\r\n waypoints: edge.waypoints,\r\n });\r\n diEdgeByFlow.set(edge.bpmnElement, edge);\r\n });\r\n if (edgeInfos.length === 0) return;\r\n\r\n const refined = refineAnchors(anchorShapes, edgeInfos, {\r\n referenceBounds: shapeBoundsById,\r\n obstacleBounds,\r\n });\r\n for (const [flowId, wps] of refined) {\r\n const edge = diEdgeByFlow.get(flowId);\r\n if (edge) {\r\n edge.waypoints = wps;\r\n }\r\n }\r\n }\r\n\r\n private restoreEdgeWaypoints(\r\n doc: BpmnDocument,\r\n snapshots: Map,\r\n ): void {\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNEdge') return;\r\n const edge = pe as BpmnElement & { waypoints?: Point[] };\r\n const snap = snapshots.get(edge.id);\r\n if (!snap) return;\r\n edge.waypoints = snap.map((p) => ({ x: p.x, y: p.y }));\r\n });\r\n }\r\n}\r\n\r\n/**\r\n * Translate a container and everything inside it as one rigid body.\r\n *\r\n * `MoveShapeCommand` moves a single BPMNShape. That is wrong for a pool:\r\n * a participant's shape is only the frame, and its lanes, flow nodes and\r\n * internal flows are separate plane elements. Moving the frame alone\r\n * separates a pool from the body the user sees (#8232). Whole-pool moves\r\n * therefore shift every member's bounds AND every member edge's\r\n * waypoints by the same delta, so the interior geometry - including\r\n * hand-placed bends and labels - is preserved exactly.\r\n *\r\n * `memberIds` is the closure of the moved tree: the root plus every\r\n * element the caller determined travels with it. Edges with BOTH\r\n * endpoints in that closure translate rigidly; edges with exactly one\r\n * endpoint inside (a message flow to another pool) are re-anchored on\r\n * the moved end so they still terminate on their shape.\r\n */\r\nexport class MoveShapeTreeCommand implements Command {\r\n public readonly kind: string = 'moveShapeTree';\r\n public readonly description: string;\r\n private readonly members: Set;\r\n private readonly dx: number;\r\n private readonly dy: number;\r\n // Populated on first execute; reused on redo and consumed by undo.\r\n private straddlingSnapshots: Map | null = null;\r\n // Data association ends, resolved ONCE before the first execute moves\r\n // anything. A multi-source input association picks its visible source\r\n // by geometry, so resolving again after the move could pick a different\r\n // source than the one snapshotted for undo (#9580).\r\n private dataEnds: Map | null = null;\r\n\r\n public constructor(\r\n rootId: string,\r\n memberIds: readonly string[],\r\n dx: number,\r\n dy: number,\r\n ) {\r\n this.members = new Set([rootId, ...memberIds]);\r\n this.dx = dx;\r\n this.dy = dy;\r\n this.description = `Move ${rootId} and its contents by (${String(\r\n dx,\r\n )}, ${String(dy)})`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n this.pinnedDataEnds(doc);\r\n if (this.straddlingSnapshots === null) {\r\n this.straddlingSnapshots = this.snapshotStraddlingEdges(doc);\r\n }\r\n this.translate(doc, this.dx, this.dy);\r\n this.reanchorStraddlingEdges(doc);\r\n }\r\n\r\n private pinnedDataEnds(doc: BpmnDocument): Map {\r\n if (this.dataEnds === null) {\r\n this.dataEnds = collectDataAssociationEnds(doc);\r\n }\r\n return this.dataEnds;\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n this.translate(doc, -this.dx, -this.dy);\r\n if (this.straddlingSnapshots !== null) {\r\n this.restoreWaypoints(doc, this.straddlingSnapshots);\r\n }\r\n }\r\n\r\n /** Shift every member shape and member edge by (dx, dy). */\r\n private translate(doc: BpmnDocument, dx: number, dy: number): void {\r\n if (dx === 0 && dy === 0) return;\r\n walkPlaneElements(doc, (pe) => {\r\n const owner = (pe as BpmnElement & { bpmnElement?: string }).bpmnElement;\r\n if (owner === undefined || !this.members.has(owner)) return;\r\n const el = pe as BpmnElement & {\r\n bounds?: { x: number; y: number; width: number; height: number };\r\n waypoints?: Point[];\r\n label?: {\r\n bounds?: { x: number; y: number; width: number; height: number };\r\n };\r\n };\r\n if (el.bounds) {\r\n el.bounds.x += dx;\r\n el.bounds.y += dy;\r\n }\r\n if (Array.isArray(el.waypoints)) {\r\n for (const wp of el.waypoints) {\r\n wp.x += dx;\r\n wp.y += dy;\r\n }\r\n }\r\n if (el.label?.bounds) {\r\n el.label.bounds.x += dx;\r\n el.label.bounds.y += dy;\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Edges with exactly one endpoint inside the moved tree. Their\r\n * waypoints are NOT translated (the outside end must stay put), so\r\n * they need re-anchoring after the move.\r\n */\r\n private collectStraddlingEdges(\r\n doc: BpmnDocument,\r\n ): (BpmnElement & { bpmnElement: string; waypoints?: Point[] })[] {\r\n const result: (BpmnElement & {\r\n bpmnElement: string;\r\n waypoints?: Point[];\r\n })[] = [];\r\n const dataEnds = this.pinnedDataEnds(doc);\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNEdge') return;\r\n const edge = pe as BpmnElement & {\r\n bpmnElement: string;\r\n waypoints?: Point[];\r\n };\r\n if (!edge.waypoints || edge.waypoints.length < 2) return;\r\n if (this.members.has(edge.bpmnElement)) return;\r\n const flow = doc.byId.get(edge.bpmnElement);\r\n if (!flow) return;\r\n const { sourceRef, targetRef } = resolveEdgeEnds(flow, dataEnds);\r\n if (sourceRef === undefined || targetRef === undefined) return;\r\n if (this.members.has(sourceRef) === this.members.has(targetRef)) return;\r\n result.push(edge);\r\n });\r\n return result;\r\n }\r\n\r\n private snapshotStraddlingEdges(doc: BpmnDocument): Map {\r\n const snap = new Map();\r\n for (const edge of this.collectStraddlingEdges(doc)) {\r\n if (!edge.waypoints) continue;\r\n snap.set(\r\n edge.id,\r\n edge.waypoints.map((p) => ({ x: p.x, y: p.y })),\r\n );\r\n }\r\n return snap;\r\n }\r\n\r\n private reanchorStraddlingEdges(doc: BpmnDocument): void {\r\n const straddling = this.collectStraddlingEdges(doc);\r\n if (straddling.length === 0) return;\r\n const boundsById = new Map();\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNShape') return;\r\n const shape = pe as BpmnElement & {\r\n bpmnElement: string;\r\n bounds?: Rect;\r\n };\r\n if (!shape.bpmnElement || !shape.bounds) return;\r\n boundsById.set(shape.bpmnElement, shape.bounds);\r\n });\r\n const dataEnds = this.pinnedDataEnds(doc);\r\n for (const edge of straddling) {\r\n const waypoints = edge.waypoints;\r\n if (!waypoints) continue;\r\n const flow = doc.byId.get(edge.bpmnElement);\r\n if (!flow) continue;\r\n // Same end resolution as collectStraddlingEdges, so a data\r\n // association picked up there is re-anchored here too (#9580).\r\n const { sourceRef, targetRef } = resolveEdgeEnds(flow, dataEnds);\r\n if (sourceRef === undefined || targetRef === undefined) continue;\r\n const sourceRect = boundsById.get(sourceRef);\r\n const targetRect = boundsById.get(targetRef);\r\n if (!sourceRect || !targetRect) continue;\r\n edge.waypoints = this.members.has(sourceRef)\r\n ? reanchorEnd(waypoints, sourceRect, 'source', rectCenter(targetRect))\r\n : reanchorEnd(waypoints, targetRect, 'target', rectCenter(sourceRect));\r\n }\r\n }\r\n\r\n private restoreWaypoints(\r\n doc: BpmnDocument,\r\n snapshots: Map,\r\n ): void {\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNEdge') return;\r\n const edge = pe as BpmnElement & { waypoints?: Point[] };\r\n const snap = snapshots.get(edge.id);\r\n if (!snap) return;\r\n edge.waypoints = snap.map((p) => ({ x: p.x, y: p.y }));\r\n });\r\n }\r\n}\r\n\r\n/**\r\n * True when either endpoint of `waypoints` docks on a border side of\r\n * its shape that faces AWAY from the other shape's center — a flow\r\n * that loops around and enters its target from the far side. Floating\r\n * endpoints that are not on the border at all are unclassifiable and\r\n * do not trigger (they are handled by the cost comparison alone).\r\n */\r\nfunction hasWrongSideDock(\r\n waypoints: readonly Point[],\r\n sourceRect: Rect,\r\n targetRect: Rect,\r\n): boolean {\r\n const first = waypoints[0];\r\n const last = waypoints[waypoints.length - 1];\r\n if (first !== undefined) {\r\n const side = dockSide(sourceRect, first);\r\n if (side !== null && !sideFacesPoint(sourceRect, side, rectCenter(targetRect))) {\r\n return true;\r\n }\r\n }\r\n if (last !== undefined) {\r\n const side = dockSide(targetRect, last);\r\n if (side !== null && !sideFacesPoint(targetRect, side, rectCenter(sourceRect))) {\r\n return true;\r\n }\r\n }\r\n return false;\r\n}\r\n\r\n/**\r\n * Rewrite one endpoint of an orthogonal flow so it lands on the\r\n * correct side port of `shape`, inserting a single orthogonal elbow\r\n * bend when the first (or last) segment would otherwise be diagonal.\r\n * Interior waypoints are preserved.\r\n *\r\n * `end === 'source'` means waypoints[0] is the shape-attached endpoint\r\n * (and waypoints[1] is its neighbour toward the rest of the path).\r\n * `end === 'target'` means waypoints[last] is the shape-attached\r\n * endpoint (and waypoints[last-1] is its neighbour).\r\n *\r\n * Exported because the routing pipeline (R3 elbow router) and the\r\n * \"straighten\" action (R2) reuse it.\r\n */\r\nexport function reanchorEnd(\r\n waypoints: readonly Point[],\r\n shape: Rect,\r\n end: 'source' | 'target',\r\n sideReference?: Point,\r\n): Point[] {\r\n if (waypoints.length < 2) {\r\n return waypoints.map((p) => ({ x: p.x, y: p.y }));\r\n }\r\n const endpointIndex = end === 'source' ? 0 : waypoints.length - 1;\r\n const neighborIndex = end === 'source' ? 1 : waypoints.length - 2;\r\n const neighbor = waypoints[neighborIndex];\r\n if (neighbor === undefined) {\r\n return waypoints.map((p) => ({ x: p.x, y: p.y }));\r\n }\r\n // Side selection: prefer the side facing the immediate neighbour so\r\n // an already-orthogonal first leg stays byte-identical (no elbow\r\n // inserted, interior bends preserved). But fall back to the\r\n // caller-supplied reference (the OTHER shape's centroid) when the\r\n // neighbour-derived side faces *away* from that reference — that is\r\n // the symptom of a stale interior bend, where the immediate\r\n // neighbour is no longer pointing toward the actual other end of\r\n // the flow and `sideToward` would otherwise pick the wrong edge.\r\n let side: Side = sideToward(shape, neighbor);\r\n if (sideReference !== undefined && !sideFacesPoint(shape, side, sideReference)) {\r\n side = sideToward(shape, sideReference);\r\n }\r\n const newEndpoint: Point = sidePort(shape, side);\r\n\r\n const next: Point[] = waypoints.map((p) => ({ x: p.x, y: p.y }));\r\n next[endpointIndex] = newEndpoint;\r\n\r\n const diag =\r\n newEndpoint.x !== neighbor.x && newEndpoint.y !== neighbor.y;\r\n if (diag) {\r\n // Insert a single elbow bend between the endpoint and its\r\n // neighbour so the first (or last) segment is axis-aligned. The\r\n // elbow side depends on which side of the shape the endpoint now\r\n // exits from: left/right -> horizontal-first, top/bottom ->\r\n // vertical-first.\r\n const elbow: Point =\r\n side === 'left' || side === 'right'\r\n ? { x: neighbor.x, y: newEndpoint.y }\r\n : { x: newEndpoint.x, y: neighbor.y };\r\n if (end === 'source') {\r\n next.splice(1, 0, elbow);\r\n } else {\r\n next.splice(next.length - 1, 0, elbow);\r\n }\r\n }\r\n\r\n return coalesceCollinear(next);\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* InsertElementsCommand — paste / duplicate */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Input shape for an insertion — the semantic element, its parent\r\n * container array, and its corresponding BPMNShape with the plane it\r\n * should live in. All fields are required so undo/redo is mechanical.\r\n */\r\nexport interface ElementInsert {\r\n element: BpmnElement;\r\n parent: BpmnElement[];\r\n diShape: BpmnElement;\r\n plane: BpmnElement[];\r\n}\r\n\r\n/**\r\n * Inserts a batch of new elements into the document. Used by paste\r\n * and duplicate. Undo removes every inserted element from its\r\n * remembered slot.\r\n */\r\nexport class InsertElementsCommand implements Command {\r\n public readonly kind: string = 'insertElements';\r\n public readonly description: string;\r\n private readonly inserts: ElementInsert[];\r\n private readonly insertedIndices: { parent: BpmnElement[]; index: number; element: BpmnElement }[] = [];\r\n private readonly insertedDiIndices: { parent: BpmnElement[]; index: number; element: BpmnElement }[] = [];\r\n private executed: boolean = false;\r\n\r\n public constructor(inserts: readonly ElementInsert[], description?: string) {\r\n this.inserts = inserts.slice();\r\n this.description = description ?? `Insert ${String(inserts.length)} element${inserts.length === 1 ? '' : 's'}`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n for (const ins of this.inserts) {\r\n ins.parent.push(ins.element);\r\n this.insertedIndices.push({\r\n parent: ins.parent,\r\n index: ins.parent.length - 1,\r\n element: ins.element,\r\n });\r\n ins.plane.push(ins.diShape);\r\n this.insertedDiIndices.push({\r\n parent: ins.plane,\r\n index: ins.plane.length - 1,\r\n element: ins.diShape,\r\n });\r\n doc.byId.set(ins.element.id, ins.element);\r\n }\r\n this.executed = true;\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n if (!this.executed) return;\r\n // Remove by reference, not index — indexes drift as siblings change.\r\n for (const ins of this.inserts) {\r\n const i1 = ins.parent.indexOf(ins.element);\r\n if (i1 >= 0) ins.parent.splice(i1, 1);\r\n const i2 = ins.plane.indexOf(ins.diShape);\r\n if (i2 >= 0) ins.plane.splice(i2, 1);\r\n doc.byId.delete(ins.element.id);\r\n }\r\n }\r\n\r\n /** Ids of elements inserted — useful for selecting after paste. */\r\n public getInsertedIds(): string[] {\r\n return this.inserts.map((i) => i.element.id);\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* CompositeCommand — one undo step, many atomic commands */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Bundles multiple commands into a single undoable unit. Useful for\r\n * group operations (multi-select move, multi-delete) where the user\r\n * expects one Ctrl+Z to reverse everything.\r\n *\r\n * Execute order: commands are applied forward in array order.\r\n * Undo order: reverse — last applied is first undone.\r\n */\r\nexport class CompositeCommand implements Command {\r\n public readonly kind: string = 'composite';\r\n public readonly description: string;\r\n private readonly children: Command[];\r\n\r\n public constructor(children: readonly Command[], description?: string) {\r\n this.children = children.slice();\r\n this.description =\r\n description ??\r\n `${String(this.children.length)} commands: ${this.children\r\n .map((c) => c.kind)\r\n .join(', ')}`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n for (const child of this.children) {\r\n child.execute(doc);\r\n }\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n for (let i = this.children.length - 1; i >= 0; i -= 1) {\r\n this.children[i]?.undo(doc);\r\n }\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* SetElementTypeCommand — retype a shape within the same family */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Swap an element's `type` attribute to a different value. Used to\r\n * convert between task subtypes (task ↔ userTask ↔ serviceTask ↔ …)\r\n * and between gateway subtypes (exclusiveGateway ↔ parallelGateway\r\n * ↔ …). The element's id and all other attributes are preserved so\r\n * edges referencing it stay valid.\r\n *\r\n * Callers are responsible for enforcing allowed transitions; the\r\n * command itself trusts its input.\r\n */\r\nexport class SetElementTypeCommand implements Command {\r\n public readonly kind: string = 'setElementType';\r\n public readonly description: string;\r\n private readonly elementId: string;\r\n private readonly previousType: string;\r\n private readonly nextType: string;\r\n\r\n public constructor(elementId: string, previousType: string, nextType: string) {\r\n this.elementId = elementId;\r\n this.previousType = previousType;\r\n this.nextType = nextType;\r\n this.description = `Change ${elementId}: ${previousType} -> ${nextType}`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n this.apply(doc, this.nextType);\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n this.apply(doc, this.previousType);\r\n }\r\n\r\n private apply(doc: BpmnDocument, nextType: string): void {\r\n const element = doc.byId.get(this.elementId);\r\n if (!element) return;\r\n (element as { type: string }).type = nextType;\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* ResizeShapeCommand — change a BPMNShape's bounds */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Bounds shape, duplicated here to avoid importing the DI model. */\r\nexport interface ResizeBounds {\r\n x: number;\r\n y: number;\r\n width: number;\r\n height: number;\r\n}\r\n\r\n/**\r\n * A remembered width/height with no position - what\r\n * {@link SetShapeExpandedCommand} carries so an expansion can restore the\r\n * size a collapse took away (Bug #8311). Position is deliberately absent:\r\n * the shape may have been dragged elsewhere while it was collapsed, and\r\n * expanding it must not move it back.\r\n */\r\nexport interface RetainedSize {\r\n width: number;\r\n height: number;\r\n}\r\n\r\n/**\r\n * Swap a BPMNShape's bounds to a new `(x, y, width, height)` rectangle.\r\n * Callers hand in the explicit \"before\" and \"after\" bounds — gesture\r\n * code has already resolved the corner math. Label bounds come along\r\n * only when `shiftLabel` is true (corner-resize typically repositions\r\n * the label relative to the shape; with `false` the label stays put\r\n * in scene coordinates, which is usually fine for tasks).\r\n */\r\nexport class ResizeShapeCommand implements Command {\r\n public readonly kind: string = 'resizeShape';\r\n public readonly description: string;\r\n private readonly elementId: string;\r\n private readonly before: ResizeBounds;\r\n private readonly after: ResizeBounds;\r\n\r\n public constructor(elementId: string, before: ResizeBounds, after: ResizeBounds) {\r\n this.elementId = elementId;\r\n this.before = { ...before };\r\n this.after = { ...after };\r\n this.description = `Resize ${elementId}`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n this.apply(doc, this.before, this.after);\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n this.apply(doc, this.after, this.before);\r\n }\r\n\r\n private apply(doc: BpmnDocument, from: ResizeBounds, to: ResizeBounds): void {\r\n // Re-anchor attached boundary events BEFORE the host bounds change,\r\n // so their fractional position is measured against the host's\r\n // current (from) box and re-projected onto the new (to) box.\r\n reanchorBoundaryEvents(doc, this.elementId, from, to);\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNShape') return;\r\n const shape = pe as BpmnElement & {\r\n bpmnElement: string;\r\n bounds?: ResizeBounds;\r\n };\r\n if (shape.bpmnElement !== this.elementId) return;\r\n if (!shape.bounds) return;\r\n shape.bounds.x = to.x;\r\n shape.bounds.y = to.y;\r\n shape.bounds.width = to.width;\r\n shape.bounds.height = to.height;\r\n });\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* RerouteFlowCommand — change an edge's source or target */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Which endpoint of a flow is being rerouted. */\r\nexport type FlowEndpoint = 'source' | 'target';\r\n\r\n/**\r\n * Reattach one endpoint of a sequence/message flow to a different\r\n * shape. Updates both the semantic reference (flow.sourceRef or\r\n * flow.targetRef) and the corresponding BPMNEdge waypoint.\r\n *\r\n * Waypoint strategy: the endpoint waypoint moves to the new shape's\r\n * centre. Intermediate waypoints are preserved. This is naive for\r\n * long flows that detour through many waypoints — for clean\r\n * authoring BPMN the first/last waypoint is always AT the shape, so\r\n * replacing just that one is correct.\r\n */\r\nexport class RerouteFlowCommand implements Command {\r\n public readonly kind: string = 'rerouteFlow';\r\n public readonly description: string;\r\n private readonly flowId: string;\r\n private readonly endpoint: FlowEndpoint;\r\n private readonly previousShapeId: string;\r\n private readonly previousWaypoints: { x: number; y: number }[];\r\n private readonly nextShapeId: string;\r\n private readonly nextWaypoints: { x: number; y: number }[];\r\n\r\n /**\r\n * Carries the FULL waypoint polylines for both states. The moved\r\n * end used to be re-pointed at the new shape's centre with the rest\r\n * of the polyline left alone, which buried the line inside the\r\n * shape body — reconnect now routes properly (#7902), so the whole\r\n * path swaps.\r\n */\r\n public constructor(\r\n flowId: string,\r\n endpoint: FlowEndpoint,\r\n previousShapeId: string,\r\n previousWaypoints: readonly { x: number; y: number }[],\r\n nextShapeId: string,\r\n nextWaypoints: readonly { x: number; y: number }[],\r\n ) {\r\n this.flowId = flowId;\r\n this.endpoint = endpoint;\r\n this.previousShapeId = previousShapeId;\r\n this.previousWaypoints = previousWaypoints.map((p) => ({ x: p.x, y: p.y }));\r\n this.nextShapeId = nextShapeId;\r\n this.nextWaypoints = nextWaypoints.map((p) => ({ x: p.x, y: p.y }));\r\n this.description = `Reroute ${endpoint} of ${flowId}: ${previousShapeId} -> ${nextShapeId}`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n this.apply(doc, this.nextShapeId, this.nextWaypoints);\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n this.apply(doc, this.previousShapeId, this.previousWaypoints);\r\n }\r\n\r\n private apply(\r\n doc: BpmnDocument,\r\n attachShapeId: string,\r\n waypoints: readonly { x: number; y: number }[],\r\n ): void {\r\n const flow = doc.byId.get(this.flowId) as\r\n | (BpmnElement & { sourceRef?: string; targetRef?: string })\r\n | undefined;\r\n if (!flow) return;\r\n if (this.endpoint === 'source') {\r\n flow.sourceRef = attachShapeId;\r\n } else {\r\n flow.targetRef = attachShapeId;\r\n }\r\n // Swap the matching BPMNEdge's full polyline.\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNEdge') return;\r\n const edge = pe as BpmnElement & {\r\n bpmnElement: string;\r\n waypoints?: { x: number; y: number }[];\r\n };\r\n if (edge.bpmnElement !== this.flowId) return;\r\n if (!edge.waypoints || edge.waypoints.length < 2) return;\r\n edge.waypoints = waypoints.map((p) => ({ x: p.x, y: p.y }));\r\n });\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* SetEventDefinitionCommand — set/clear event definition */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Set or clear the `eventDefinition` slot on an event element\r\n * (startEvent, endEvent, intermediateThrowEvent, intermediateCatchEvent,\r\n * boundaryEvent). Passing `null` clears the definition (plain event).\r\n *\r\n * Callers are responsible for enforcing allowed kinds per event type\r\n * (e.g. `terminate` only on endEvent, `cancel` only on endEvent in a\r\n * transaction subprocess / boundary of one). The command itself trusts\r\n * its input.\r\n */\r\nexport class SetEventDefinitionCommand implements Command {\r\n public readonly kind: string = 'setEventDefinition';\r\n public readonly description: string;\r\n private readonly elementId: string;\r\n private readonly previous: EventDefinition | undefined;\r\n private readonly next: EventDefinition | undefined;\r\n\r\n public constructor(\r\n elementId: string,\r\n previous: EventDefinition | undefined,\r\n next: EventDefinition | undefined,\r\n ) {\r\n this.elementId = elementId;\r\n this.previous = previous !== undefined ? cloneDefinition(previous) : undefined;\r\n this.next = next !== undefined ? cloneDefinition(next) : undefined;\r\n const nextLabel = next?.kind ?? 'none';\r\n this.description = `Set ${elementId} event definition -> ${nextLabel}`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n this.apply(doc, this.next);\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n this.apply(doc, this.previous);\r\n }\r\n\r\n private apply(doc: BpmnDocument, value: EventDefinition | undefined): void {\r\n const element = doc.byId.get(this.elementId);\r\n if (!element) return;\r\n const target = element as BpmnElement & { eventDefinition?: EventDefinition };\r\n if (value === undefined) {\r\n delete target.eventDefinition;\r\n } else {\r\n target.eventDefinition = cloneDefinition(value);\r\n }\r\n }\r\n}\r\n\r\nfunction cloneDefinition(def: EventDefinition): EventDefinition {\r\n return { ...def } as EventDefinition;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* SetFlowConditionCommand — set/clear conditionExpression */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Set or clear the `conditionExpression` text on a sequence flow. Pass\r\n * `undefined` (or empty string — callers decide which) to clear.\r\n * Callers are responsible for verifying the element is a sequence\r\n * flow; the command trusts its input.\r\n */\r\nexport class SetFlowConditionCommand implements Command {\r\n public readonly kind: string = 'setFlowCondition';\r\n public readonly description: string;\r\n private readonly flowId: string;\r\n private readonly previous: string | undefined;\r\n private readonly next: string | undefined;\r\n\r\n public constructor(\r\n flowId: string,\r\n previous: string | undefined,\r\n next: string | undefined,\r\n ) {\r\n this.flowId = flowId;\r\n this.previous = previous;\r\n this.next = next;\r\n this.description = `Set ${flowId} condition`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n this.apply(doc, this.next);\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n this.apply(doc, this.previous);\r\n }\r\n\r\n private apply(doc: BpmnDocument, value: string | undefined): void {\r\n const element = doc.byId.get(this.flowId);\r\n if (!element) return;\r\n const target = element as BpmnElement & { conditionExpression?: string };\r\n if (value === undefined) {\r\n delete target.conditionExpression;\r\n } else {\r\n target.conditionExpression = value;\r\n }\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* SetDefaultFlowCommand — mark a flow as the source's default */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Set or clear the `default` attribute on a gateway or activity. The\r\n * value is the id of an outgoing sequence flow (or `undefined` to\r\n * clear). Callers are responsible for verifying the flow actually\r\n * originates from the source element; the command trusts its input.\r\n */\r\nexport class SetDefaultFlowCommand implements Command {\r\n public readonly kind: string = 'setDefaultFlow';\r\n public readonly description: string;\r\n private readonly sourceId: string;\r\n private readonly previous: string | undefined;\r\n private readonly next: string | undefined;\r\n\r\n public constructor(\r\n sourceId: string,\r\n previous: string | undefined,\r\n next: string | undefined,\r\n ) {\r\n this.sourceId = sourceId;\r\n this.previous = previous;\r\n this.next = next;\r\n this.description = `Set ${sourceId} default flow -> ${next ?? '(none)'}`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n this.apply(doc, this.next);\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n this.apply(doc, this.previous);\r\n }\r\n\r\n private apply(doc: BpmnDocument, value: string | undefined): void {\r\n const element = doc.byId.get(this.sourceId);\r\n if (!element) return;\r\n const target = element as BpmnElement & { default?: string };\r\n if (value === undefined) {\r\n delete target.default;\r\n } else {\r\n target.default = value;\r\n }\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* SetActivityLoopCommand — set/clear loopCharacteristics */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Set or clear `loopCharacteristics` on an activity. Callers supply\r\n * the new LoopCharacteristics object (or `undefined` to clear). The\r\n * command preserves the previous value for undo.\r\n *\r\n * Callers are responsible for enforcing that the target element is an\r\n * activity; the command trusts its input.\r\n */\r\nexport class SetActivityLoopCommand implements Command {\r\n public readonly kind: string = 'setActivityLoop';\r\n public readonly description: string;\r\n private readonly elementId: string;\r\n private readonly previous: LoopCharacteristics | undefined;\r\n private readonly next: LoopCharacteristics | undefined;\r\n\r\n public constructor(\r\n elementId: string,\r\n previous: LoopCharacteristics | undefined,\r\n next: LoopCharacteristics | undefined,\r\n ) {\r\n this.elementId = elementId;\r\n this.previous = previous !== undefined ? cloneLoop(previous) : undefined;\r\n this.next = next !== undefined ? cloneLoop(next) : undefined;\r\n const label = next?.kind ?? 'none';\r\n this.description = `Set ${elementId} loop -> ${label}`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n this.apply(doc, this.next);\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n this.apply(doc, this.previous);\r\n }\r\n\r\n private apply(doc: BpmnDocument, value: LoopCharacteristics | undefined): void {\r\n const element = doc.byId.get(this.elementId);\r\n if (!element) return;\r\n const target = element as BpmnElement & { loopCharacteristics?: LoopCharacteristics };\r\n if (value === undefined) {\r\n delete target.loopCharacteristics;\r\n } else {\r\n target.loopCharacteristics = cloneLoop(value);\r\n }\r\n }\r\n}\r\n\r\nfunction cloneLoop(loop: LoopCharacteristics): LoopCharacteristics {\r\n return { ...loop } as LoopCharacteristics;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* SetShapeExpandedCommand - DI isExpanded + the bounds it is drawn at */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Set the `isExpanded` attribute on the BPMNShape bound to `elementId`,\r\n * and move its bounds to the size that state is drawn at.\r\n *\r\n * `isExpanded` is a DI attribute, not a semantic one: it decides whether\r\n * a sub-process is drawn as an open container body or as a task-sized box\r\n * carrying the \"+\" marker (`view/renderers/subprocesses.ts`). The two\r\n * states are drawn at different sizes, so the bounds move with the flag -\r\n * a collapsed sub-process left at container size would be a large empty\r\n * box with a small \"+\" in the bottom edge, which is not what the marker\r\n * means. Both halves are one command so a single Ctrl+Z restores the\r\n * exact previous flag AND the exact previous rectangle.\r\n *\r\n * `previous` is the flag as it was, INCLUDING `undefined` for \"the\r\n * attribute was absent\" - an absent `isExpanded` reads as expanded, and\r\n * undo has to put the file back the way it was rather than materialising\r\n * an attribute the document never had.\r\n *\r\n * The command carries a THIRD piece of state for the same reason it\r\n * carries the rectangle: `retainedExpandedSize`, the size the shape was\r\n * drawn at when it was collapsed, which expanding it again restores\r\n * (Bug #8311). Collapsing sets it, expanding clears it, and undo puts back\r\n * whichever value was there before - so a Ctrl+Z after a collapse leaves\r\n * no retained size behind to be applied to some later expansion, and a\r\n * Ctrl+Z after an expansion puts the retained size back so a redo can\r\n * consume it again.\r\n *\r\n * Callers are responsible for deciding that the element may change state\r\n * (it is a sub-process-family element, and collapsing it would not strand\r\n * contents it holds); the command trusts its input.\r\n */\r\nexport class SetShapeExpandedCommand implements Command {\r\n public readonly kind: string = 'setShapeExpanded';\r\n public readonly description: string;\r\n private readonly elementId: string;\r\n private readonly previousExpanded: boolean | undefined;\r\n private readonly nextExpanded: boolean;\r\n private readonly previousBounds: ResizeBounds | undefined;\r\n private readonly nextBounds: ResizeBounds | undefined;\r\n private readonly previousRetainedSize: RetainedSize | undefined;\r\n private readonly nextRetainedSize: RetainedSize | undefined;\r\n\r\n public constructor(\r\n elementId: string,\r\n previousExpanded: boolean | undefined,\r\n nextExpanded: boolean,\r\n previousBounds: ResizeBounds | undefined,\r\n nextBounds: ResizeBounds | undefined,\r\n previousRetainedSize?: RetainedSize,\r\n nextRetainedSize?: RetainedSize,\r\n ) {\r\n this.elementId = elementId;\r\n this.previousExpanded = previousExpanded;\r\n this.nextExpanded = nextExpanded;\r\n this.previousBounds = previousBounds !== undefined ? { ...previousBounds } : undefined;\r\n this.nextBounds = nextBounds !== undefined ? { ...nextBounds } : undefined;\r\n this.previousRetainedSize =\r\n previousRetainedSize !== undefined ? { ...previousRetainedSize } : undefined;\r\n this.nextRetainedSize =\r\n nextRetainedSize !== undefined ? { ...nextRetainedSize } : undefined;\r\n this.description = `Set ${elementId} expanded -> ${String(nextExpanded)}`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n this.apply(doc, this.nextExpanded, this.nextBounds, this.nextRetainedSize);\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n this.apply(doc, this.previousExpanded, this.previousBounds, this.previousRetainedSize);\r\n }\r\n\r\n private apply(\r\n doc: BpmnDocument,\r\n expanded: boolean | undefined,\r\n bounds: ResizeBounds | undefined,\r\n retainedSize: RetainedSize | undefined,\r\n ): void {\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNShape') return;\r\n const shape = pe as BpmnElement & {\r\n bpmnElement: string;\r\n bounds?: ResizeBounds;\r\n isExpanded?: boolean;\r\n retainedExpandedSize?: RetainedSize;\r\n };\r\n if (shape.bpmnElement !== this.elementId) return;\r\n if (expanded === undefined) {\r\n delete shape.isExpanded;\r\n } else {\r\n shape.isExpanded = expanded;\r\n }\r\n if (bounds !== undefined && shape.bounds) {\r\n shape.bounds.x = bounds.x;\r\n shape.bounds.y = bounds.y;\r\n shape.bounds.width = bounds.width;\r\n shape.bounds.height = bounds.height;\r\n }\r\n if (retainedSize === undefined) {\r\n delete shape.retainedExpandedSize;\r\n } else {\r\n shape.retainedExpandedSize = { ...retainedSize };\r\n }\r\n });\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* SetWaypointsCommand — replace the waypoint polyline on an edge */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Replace the `waypoints` array on a BPMNEdge bound to `flowId`.\r\n * Used by bendpoint editing: drag an interior waypoint, produce a new\r\n * array, command handles undo via the saved previous snapshot. The\r\n * constructor deep-clones both arrays so later mutation of the inputs\r\n * doesn't leak into the stored deltas.\r\n */\r\nexport class SetWaypointsCommand implements Command {\r\n public readonly kind: string = 'setWaypoints';\r\n public readonly description: string;\r\n private readonly flowId: string;\r\n private readonly previous: { x: number; y: number }[];\r\n private readonly next: { x: number; y: number }[];\r\n\r\n public constructor(\r\n flowId: string,\r\n previous: readonly { x: number; y: number }[],\r\n next: readonly { x: number; y: number }[],\r\n ) {\r\n this.flowId = flowId;\r\n this.previous = previous.map((p) => ({ x: p.x, y: p.y }));\r\n this.next = next.map((p) => ({ x: p.x, y: p.y }));\r\n this.description = `Set ${flowId} waypoints (${String(next.length)} points)`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n this.apply(doc, this.next);\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n this.apply(doc, this.previous);\r\n }\r\n\r\n private apply(\r\n doc: BpmnDocument,\r\n points: readonly { x: number; y: number }[],\r\n ): void {\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNEdge') return;\r\n const edge = pe as BpmnElement & {\r\n bpmnElement: string;\r\n waypoints?: { x: number; y: number }[];\r\n };\r\n if (edge.bpmnElement !== this.flowId) return;\r\n edge.waypoints = points.map((p) => ({ x: p.x, y: p.y }));\r\n });\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* SetEdgeLabelBoundsCommand — place a flow's label box */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Options for {@link SetEdgeLabelBoundsCommand}. */\r\nexport interface SetEdgeLabelBoundsOptions {\r\n /**\r\n * `true` marks the label as placed by a person, so Auto layout (and\r\n * any other layout pass that reads the pin) leaves it alone (#8159);\r\n * `false` clears the pin and returns the label to layout's control.\r\n * Omitted = leave whatever pin state the label already has, which is\r\n * what auto-layout's own re-placement wants.\r\n */\r\n manuallyPlaced?: boolean;\r\n}\r\n\r\n/**\r\n * Set (or create) the BPMNLabel bounds on the BPMNEdge bound to\r\n * `flowId` (#7904 - auto-layout re-places labels so they do not sit\r\n * on lines). The first execute snapshots the edge's existing label\r\n * object so undo restores it verbatim - including a label that\r\n * existed without bounds, or no label at all.\r\n *\r\n * #8159: the same command also carries the manual-placement pin, so a\r\n * user's drag is ONE undoable step that moves the box and claims\r\n * ownership of it together - undo can never leave a label pinned at a\r\n * position the user just undid.\r\n */\r\n/**\r\n * Move a SHAPE's external label - a gateway question or an event name -\r\n * to a planned position (#8247).\r\n *\r\n * The sibling of {@link SetEdgeLabelBoundsCommand} for the other kind of\r\n * label. Auto-layout previously had no way to move one: `MoveShapeCommand`\r\n * translates a shape's label rigidly with the shape, so a re-arranged\r\n * diagram carried every label along at its old offset and dropped it on\r\n * whatever now occupies that spot. Undo restores the exact previous box,\r\n * including the case where the shape had no label bounds at all.\r\n */\r\nexport class SetShapeLabelBoundsCommand implements Command {\r\n public readonly kind: string = 'setShapeLabelBounds';\r\n public readonly description: string;\r\n private readonly shapeId: string;\r\n private readonly next: RectBoundsLike;\r\n /** Captured on first execute; `null` = shape had no label. */\r\n private previousBounds: RectBoundsLike | null | undefined;\r\n private createdLabel = false;\r\n\r\n public constructor(shapeId: string, next: RectBoundsLike) {\r\n this.shapeId = shapeId;\r\n this.next = { ...next };\r\n this.description = `Place ${shapeId} label`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNShape') return;\r\n const shape = pe as BpmnElement & {\r\n bpmnElement: string;\r\n label?: BpmnElement & { bounds?: RectBoundsLike };\r\n };\r\n if (shape.bpmnElement !== this.shapeId) return;\r\n if (this.previousBounds === undefined) {\r\n this.previousBounds =\r\n shape.label?.bounds !== undefined ? { ...shape.label.bounds } : null;\r\n this.createdLabel = shape.label === undefined;\r\n }\r\n if (shape.label !== undefined) {\r\n shape.label.bounds = { ...this.next };\r\n } else {\r\n shape.label = {\r\n type: 'BPMNLabel',\r\n id: `${shape.id}_label`,\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n bounds: { ...this.next },\r\n } as BpmnElement & { bounds: RectBoundsLike };\r\n }\r\n });\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNShape') return;\r\n const shape = pe as BpmnElement & {\r\n bpmnElement: string;\r\n label?: BpmnElement & { bounds?: RectBoundsLike };\r\n };\r\n if (shape.bpmnElement !== this.shapeId) return;\r\n if (this.createdLabel) {\r\n delete shape.label;\r\n return;\r\n }\r\n if (shape.label === undefined) return;\r\n if (this.previousBounds === null) {\r\n delete shape.label.bounds;\r\n } else if (this.previousBounds !== undefined) {\r\n shape.label.bounds = { ...this.previousBounds };\r\n }\r\n });\r\n }\r\n}\r\n\r\nexport class SetEdgeLabelBoundsCommand implements Command {\r\n public readonly kind: string = 'setEdgeLabelBounds';\r\n public readonly description: string;\r\n private readonly flowId: string;\r\n private readonly next: RectBoundsLike;\r\n private readonly manuallyPlaced: boolean | undefined;\r\n /** Captured on first execute; `null` = edge had no label. */\r\n private previousLabel: (BpmnElement & { bounds?: RectBoundsLike }) | null | undefined;\r\n\r\n public constructor(\r\n flowId: string,\r\n next: RectBoundsLike,\r\n options: SetEdgeLabelBoundsOptions = {},\r\n ) {\r\n this.flowId = flowId;\r\n this.next = { ...next };\r\n this.manuallyPlaced = options.manuallyPlaced;\r\n this.description = `Place ${flowId} label`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNEdge') return;\r\n const edge = pe as BpmnElement & {\r\n bpmnElement: string;\r\n label?: BpmnElement & { bounds?: RectBoundsLike };\r\n };\r\n if (edge.bpmnElement !== this.flowId) return;\r\n if (this.previousLabel === undefined) {\r\n // The foreign-attribute array must be COPIED, not aliased: the\r\n // pin write below replaces the array on the live label, and a\r\n // shared reference would let undo restore the post-pin state.\r\n this.previousLabel =\r\n edge.label !== undefined\r\n ? {\r\n ...edge.label,\r\n extensionElements: [...edge.label.extensionElements],\r\n foreignAttributes: [...edge.label.foreignAttributes],\r\n ...(edge.label.bounds !== undefined\r\n ? { bounds: { ...edge.label.bounds } }\r\n : {}),\r\n }\r\n : null;\r\n }\r\n if (edge.label !== undefined) {\r\n edge.label.bounds = { ...this.next };\r\n } else {\r\n edge.label = {\r\n type: 'BPMNLabel',\r\n id: `${edge.id}_label`,\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n bounds: { ...this.next },\r\n } as BpmnElement & { bounds: RectBoundsLike };\r\n }\r\n if (this.manuallyPlaced !== undefined) {\r\n edge.label.foreignAttributes = withLabelPlacementPin(\r\n edge.label.foreignAttributes,\r\n this.manuallyPlaced,\r\n );\r\n }\r\n });\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n walkPlaneElements(doc, (pe) => {\r\n if (pe.type !== 'BPMNEdge') return;\r\n const edge = pe as BpmnElement & {\r\n bpmnElement: string;\r\n label?: BpmnElement & { bounds?: RectBoundsLike };\r\n };\r\n if (edge.bpmnElement !== this.flowId) return;\r\n if (this.previousLabel === null || this.previousLabel === undefined) {\r\n delete edge.label;\r\n } else {\r\n edge.label = {\r\n ...this.previousLabel,\r\n extensionElements: [...this.previousLabel.extensionElements],\r\n foreignAttributes: [...this.previousLabel.foreignAttributes],\r\n ...(this.previousLabel.bounds !== undefined\r\n ? { bounds: { ...this.previousLabel.bounds } }\r\n : {}),\r\n };\r\n }\r\n });\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* ResizePoolCommand — resize the pool and adjust lanes */\r\n/* ------------------------------------------------------------------ */\r\n\r\ninterface RectBoundsLike {\r\n x: number;\r\n y: number;\r\n width: number;\r\n height: number;\r\n}\r\n\r\ninterface ShapeBoundsAdjustment {\r\n shape: BpmnElement;\r\n before: RectBoundsLike;\r\n after: RectBoundsLike;\r\n}\r\n\r\n/**\r\n * Resize the pool's BPMNShape and apply matching adjustments to a\r\n * caller-supplied list of lane shapes. The lane adjustments express\r\n * the policy chosen by the facade — currently \"trailing lane absorbs\"\r\n * along the lane axis, \"all lanes match width\" along the breadth\r\n * axis. The command itself is policy-free: it just applies the\r\n * before/after bounds in execute / undo.\r\n *\r\n * Per PHASE_9_PLAN §9d.\r\n */\r\nexport class ResizePoolCommand implements Command {\r\n public readonly kind: string = 'resizePool';\r\n public readonly description: string;\r\n private readonly pool: ShapeBoundsAdjustment;\r\n private readonly lanes: ShapeBoundsAdjustment[];\r\n\r\n public constructor(options: {\r\n pool: ShapeBoundsAdjustment;\r\n lanes: readonly ShapeBoundsAdjustment[];\r\n }) {\r\n this.pool = {\r\n shape: options.pool.shape,\r\n before: { ...options.pool.before },\r\n after: { ...options.pool.after },\r\n };\r\n this.lanes = options.lanes.map((l) => ({\r\n shape: l.shape,\r\n before: { ...l.before },\r\n after: { ...l.after },\r\n }));\r\n const bpmnElementRef = (options.pool.shape as BpmnElement & {\r\n bpmnElement?: string;\r\n }).bpmnElement;\r\n this.description = `Resize pool ${bpmnElementRef ?? options.pool.shape.id}`;\r\n }\r\n\r\n public execute(_doc: BpmnDocument): void {\r\n applyBounds(this.pool.shape, this.pool.after);\r\n for (const l of this.lanes) {\r\n applyBounds(l.shape, l.after);\r\n }\r\n }\r\n\r\n public undo(_doc: BpmnDocument): void {\r\n applyBounds(this.pool.shape, this.pool.before);\r\n for (const l of this.lanes) {\r\n applyBounds(l.shape, l.before);\r\n }\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* ResizeLaneCommand — resize a lane + optional neighbour / pool */\r\n/* ------------------------------------------------------------------ */\r\n\r\ninterface RectBounds {\r\n x: number;\r\n y: number;\r\n width: number;\r\n height: number;\r\n}\r\n\r\n/**\r\n * Apply three simultaneous bounds adjustments in one undoable step:\r\n * the resized lane, an adjacent sibling lane (whose shared edge moves\r\n * with the resized lane's edge), and optionally the enclosing pool.\r\n *\r\n * All three targets are BPMNShape references; before/after bounds are\r\n * stored verbatim and cloned so later mutation of the inputs doesn't\r\n * leak. Null `*Shape` skips the corresponding adjustment — e.g. when\r\n * the resized lane is at the pool's edge and has no adjacent sibling\r\n * on that side.\r\n *\r\n * Per PHASE_9_PLAN §9b.\r\n */\r\nexport class ResizeLaneCommand implements Command {\r\n public readonly kind: string = 'resizeLane';\r\n public readonly description: string;\r\n private readonly laneShape: BpmnElement;\r\n private readonly laneBoundsBefore: RectBounds;\r\n private readonly laneBoundsAfter: RectBounds;\r\n private readonly adjacentShape: BpmnElement | null;\r\n private readonly adjacentBoundsBefore: RectBounds | null;\r\n private readonly adjacentBoundsAfter: RectBounds | null;\r\n private readonly poolShape: BpmnElement | null;\r\n private readonly poolBoundsBefore: RectBounds | null;\r\n private readonly poolBoundsAfter: RectBounds | null;\r\n\r\n public constructor(options: {\r\n laneShape: BpmnElement;\r\n laneBoundsBefore: RectBounds;\r\n laneBoundsAfter: RectBounds;\r\n adjacentShape?: BpmnElement | null;\r\n adjacentBoundsBefore?: RectBounds;\r\n adjacentBoundsAfter?: RectBounds;\r\n poolShape?: BpmnElement | null;\r\n poolBoundsBefore?: RectBounds;\r\n poolBoundsAfter?: RectBounds;\r\n }) {\r\n this.laneShape = options.laneShape;\r\n this.laneBoundsBefore = { ...options.laneBoundsBefore };\r\n this.laneBoundsAfter = { ...options.laneBoundsAfter };\r\n this.adjacentShape = options.adjacentShape ?? null;\r\n this.adjacentBoundsBefore = options.adjacentBoundsBefore\r\n ? { ...options.adjacentBoundsBefore }\r\n : null;\r\n this.adjacentBoundsAfter = options.adjacentBoundsAfter\r\n ? { ...options.adjacentBoundsAfter }\r\n : null;\r\n this.poolShape = options.poolShape ?? null;\r\n this.poolBoundsBefore = options.poolBoundsBefore\r\n ? { ...options.poolBoundsBefore }\r\n : null;\r\n this.poolBoundsAfter = options.poolBoundsAfter\r\n ? { ...options.poolBoundsAfter }\r\n : null;\r\n const bpmnElementRef = (options.laneShape as BpmnElement & {\r\n bpmnElement?: string;\r\n }).bpmnElement;\r\n this.description = `Resize lane ${bpmnElementRef ?? options.laneShape.id}`;\r\n }\r\n\r\n public execute(_doc: BpmnDocument): void {\r\n applyBounds(this.laneShape, this.laneBoundsAfter);\r\n if (this.adjacentShape && this.adjacentBoundsAfter) {\r\n applyBounds(this.adjacentShape, this.adjacentBoundsAfter);\r\n }\r\n if (this.poolShape && this.poolBoundsAfter) {\r\n applyBounds(this.poolShape, this.poolBoundsAfter);\r\n }\r\n }\r\n\r\n public undo(_doc: BpmnDocument): void {\r\n applyBounds(this.laneShape, this.laneBoundsBefore);\r\n if (this.adjacentShape && this.adjacentBoundsBefore) {\r\n applyBounds(this.adjacentShape, this.adjacentBoundsBefore);\r\n }\r\n if (this.poolShape && this.poolBoundsBefore) {\r\n applyBounds(this.poolShape, this.poolBoundsBefore);\r\n }\r\n }\r\n}\r\n\r\nfunction applyBounds(shape: BpmnElement, bounds: RectBounds): void {\r\n const target = shape as BpmnElement & { bounds?: RectBounds };\r\n target.bounds = { ...bounds };\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* AddLaneCommand — insert a lane + its BPMNShape + pool-extend */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Insert a new lane into a process's laneSet. Caller supplies a\r\n * fully-formed lane element (flowNodeRef: []), its BPMNShape with\r\n * absolute bounds, the parent laneSet's lanes array, the plane's\r\n * planeElements array, an insertion index within the laneSet, and\r\n * optional pool-shape bounds adjustment (usually the pool grows to\r\n * include the new lane's area).\r\n *\r\n * When the pool has no laneSet at all, the caller also supplies\r\n * `laneSetInsert` and this command creates the laneSet as part of the\r\n * same undoable step (#8181).\r\n *\r\n * Per PHASE_9_PLAN §9c. Undo removes all inserted artefacts and\r\n * restores the pool's pre-insert bounds.\r\n */\r\nexport interface ShapeShift {\r\n shape: BpmnElement;\r\n dx: number;\r\n dy: number;\r\n}\r\n\r\nexport interface EdgeShift {\r\n edge: BpmnElement;\r\n dx: number;\r\n dy: number;\r\n}\r\n\r\nexport class AddLaneCommand implements Command {\r\n public readonly kind: string = 'addLane';\r\n public readonly description: string;\r\n private readonly lanesArray: BpmnElement[];\r\n private readonly planeArray: BpmnElement[];\r\n private readonly insertIndex: number;\r\n private readonly newLane: BpmnElement;\r\n private readonly newShape: BpmnElement;\r\n private readonly poolShape: BpmnElement | null;\r\n private readonly poolBoundsBefore:\r\n | { x: number; y: number; width: number; height: number }\r\n | null;\r\n private readonly poolBoundsAfter:\r\n | { x: number; y: number; width: number; height: number }\r\n | null;\r\n /**\r\n * Lanes inside the same pool that need to shift to make room for the\r\n * new lane. Used when inserting \"above\" — every lane from the\r\n * insertion index onward shifts down by one lane thickness so the\r\n * new lane occupies the freed slot.\r\n */\r\n private readonly existingLaneShifts: readonly ShapeShift[];\r\n /**\r\n * Pools (and their contained shapes / sequence-flow waypoints) that\r\n * sit *below* the modified pool in screen Y and must shift down by\r\n * one lane thickness to preserve the 40px stacking gap.\r\n */\r\n private readonly cascadeShapeShifts: readonly ShapeShift[];\r\n private readonly cascadeEdgeShifts: readonly EdgeShift[];\r\n /**\r\n * laneSet that has to be inserted together with the lane, used when\r\n * the pool had no laneSet at all so there was nothing to add a lane\r\n * to (#8181). `lanesArray` is that laneSet's own `lanes` array, so\r\n * the lane insert below still works unchanged. Undo removes the\r\n * laneSet again and the document returns to its lane-less shape.\r\n */\r\n private readonly laneSetInsert: {\r\n container: BpmnElement;\r\n laneSet: BpmnElement;\r\n } | null;\r\n private executed: boolean = false;\r\n\r\n public constructor(options: {\r\n lanesArray: BpmnElement[];\r\n planeArray: BpmnElement[];\r\n insertIndex: number;\r\n newLane: BpmnElement;\r\n newShape: BpmnElement;\r\n poolShape?: BpmnElement | null;\r\n poolBoundsBefore?: { x: number; y: number; width: number; height: number };\r\n poolBoundsAfter?: { x: number; y: number; width: number; height: number };\r\n existingLaneShifts?: readonly ShapeShift[];\r\n cascadeShifts?: {\r\n shapes: readonly ShapeShift[];\r\n edgeWaypoints: readonly EdgeShift[];\r\n };\r\n laneSetInsert?: { container: BpmnElement; laneSet: BpmnElement } | null;\r\n }) {\r\n this.lanesArray = options.lanesArray;\r\n this.planeArray = options.planeArray;\r\n this.insertIndex = options.insertIndex;\r\n this.newLane = options.newLane;\r\n this.newShape = options.newShape;\r\n this.poolShape = options.poolShape ?? null;\r\n this.poolBoundsBefore = options.poolBoundsBefore\r\n ? { ...options.poolBoundsBefore }\r\n : null;\r\n this.poolBoundsAfter = options.poolBoundsAfter\r\n ? { ...options.poolBoundsAfter }\r\n : null;\r\n this.existingLaneShifts = options.existingLaneShifts ?? [];\r\n this.cascadeShapeShifts = options.cascadeShifts?.shapes ?? [];\r\n this.cascadeEdgeShifts = options.cascadeShifts?.edgeWaypoints ?? [];\r\n this.laneSetInsert = options.laneSetInsert ?? null;\r\n this.description = `Add lane ${this.newLane.id}`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n // The laneSet goes in first when this is the pool's first lane, so\r\n // the lane lands in a laneSet that is already attached to the\r\n // process (#8181). Idempotent, because redo re-runs execute().\r\n const laneSetInsert = this.laneSetInsert;\r\n if (laneSetInsert) {\r\n const container = laneSetInsert.container as BpmnElement & {\r\n laneSets?: BpmnElement[];\r\n };\r\n const laneSets = container.laneSets ?? [];\r\n container.laneSets = laneSets;\r\n if (!laneSets.includes(laneSetInsert.laneSet)) {\r\n laneSets.push(laneSetInsert.laneSet);\r\n }\r\n doc.byId.set(laneSetInsert.laneSet.id, laneSetInsert.laneSet);\r\n }\r\n const clampedIndex = Math.max(\r\n 0,\r\n Math.min(this.insertIndex, this.lanesArray.length),\r\n );\r\n this.lanesArray.splice(clampedIndex, 0, this.newLane);\r\n this.planeArray.push(this.newShape);\r\n doc.byId.set(this.newLane.id, this.newLane);\r\n if (this.poolShape && this.poolBoundsAfter) {\r\n const target = this.poolShape as BpmnElement & {\r\n bounds?: { x: number; y: number; width: number; height: number };\r\n };\r\n target.bounds = { ...this.poolBoundsAfter };\r\n }\r\n applyShapeShifts(this.existingLaneShifts, 1);\r\n applyShapeShifts(this.cascadeShapeShifts, 1);\r\n applyEdgeShifts(this.cascadeEdgeShifts, 1);\r\n this.executed = true;\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n if (!this.executed) return;\r\n const idx = this.lanesArray.indexOf(this.newLane);\r\n if (idx >= 0) this.lanesArray.splice(idx, 1);\r\n const shapeIdx = this.planeArray.indexOf(this.newShape);\r\n if (shapeIdx >= 0) this.planeArray.splice(shapeIdx, 1);\r\n doc.byId.delete(this.newLane.id);\r\n if (this.poolShape && this.poolBoundsBefore) {\r\n const target = this.poolShape as BpmnElement & {\r\n bounds?: { x: number; y: number; width: number; height: number };\r\n };\r\n target.bounds = { ...this.poolBoundsBefore };\r\n }\r\n applyShapeShifts(this.existingLaneShifts, -1);\r\n applyShapeShifts(this.cascadeShapeShifts, -1);\r\n applyEdgeShifts(this.cascadeEdgeShifts, -1);\r\n // Drop the laneSet this command created (#8181) — last, so the\r\n // lane has already been spliced out of it.\r\n const laneSetInsert = this.laneSetInsert;\r\n if (laneSetInsert) {\r\n const container = laneSetInsert.container as BpmnElement & {\r\n laneSets?: BpmnElement[];\r\n };\r\n const laneSets = container.laneSets;\r\n if (laneSets) {\r\n const laneSetIdx = laneSets.indexOf(laneSetInsert.laneSet);\r\n if (laneSetIdx >= 0) laneSets.splice(laneSetIdx, 1);\r\n }\r\n doc.byId.delete(laneSetInsert.laneSet.id);\r\n }\r\n }\r\n}\r\n\r\nfunction applyShapeShifts(shifts: readonly ShapeShift[], sign: 1 | -1): void {\r\n for (const s of shifts) {\r\n const target = s.shape as BpmnElement & {\r\n bounds?: { x: number; y: number; width: number; height: number };\r\n };\r\n if (!target.bounds) continue;\r\n target.bounds = {\r\n x: target.bounds.x + sign * s.dx,\r\n y: target.bounds.y + sign * s.dy,\r\n width: target.bounds.width,\r\n height: target.bounds.height,\r\n };\r\n }\r\n}\r\n\r\nfunction applyEdgeShifts(shifts: readonly EdgeShift[], sign: 1 | -1): void {\r\n for (const s of shifts) {\r\n const target = s.edge as BpmnElement & {\r\n waypoints?: { x: number; y: number }[];\r\n };\r\n if (!target.waypoints) continue;\r\n for (const wp of target.waypoints) {\r\n wp.x += sign * s.dx;\r\n wp.y += sign * s.dy;\r\n }\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* InsertParticipantCommand — add a Pool to the diagram */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Inserts a new Pool (Participant) into the diagram. When the document\r\n * has no `` yet, this command also synthesizes one and\r\n * links it to the existing first `` so the pool has something\r\n * to reference. Subsequent pools share the same collaboration but each\r\n * gets a fresh `` of its own.\r\n *\r\n * The Pool is created with an initial laneSet containing one lane that\r\n * fills the pool. This means the user always has somewhere to drop\r\n * lane-aware operations and `addLaneRelative` works from day one.\r\n *\r\n * Undo cleanly reverses every artefact created — including the\r\n * collaboration root if this command was the one that created it.\r\n */\r\nexport interface InsertParticipantOptions {\r\n /** Document root container (typically `doc.rootElements`). */\r\n rootElements: BpmnElement[];\r\n /** Plane element array the pool's BPMNShape lives in. */\r\n planeArray: BpmnElement[];\r\n /** Existing process to link to (when wrapping a blank diagram). */\r\n existingProcess: BpmnElement | null;\r\n /** Pool bounds at creation time (default 600x250). */\r\n poolBounds: { x: number; y: number; width: number; height: number };\r\n /** Newly-allocated participant element id (e.g. \"Participant_1\"). */\r\n participantId: string;\r\n /** Newly-allocated process element id, used when no existingProcess. */\r\n newProcessId: string;\r\n /** Newly-allocated lane id for the pool's initial lane. */\r\n initialLaneId: string;\r\n /** Optional name for the new participant. */\r\n name?: string;\r\n}\r\n\r\nexport class InsertParticipantCommand implements Command {\r\n public readonly kind: string = 'insertParticipant';\r\n public readonly description: string;\r\n private readonly options: InsertParticipantOptions;\r\n /** Captured at first execute so undo can reverse cleanly. */\r\n private state: {\r\n createdCollaboration: BpmnElement | null;\r\n collaboration: BpmnElement;\r\n process: BpmnElement;\r\n participant: BpmnElement;\r\n laneSet: BpmnElement;\r\n initialLane: BpmnElement;\r\n poolShape: BpmnElement;\r\n laneShape: BpmnElement;\r\n /** Index where collaboration was inserted (null = pre-existing). */\r\n collaborationInsertIndex: number | null;\r\n /** Index where process was inserted (null = pre-existing). */\r\n processInsertIndex: number | null;\r\n } | null = null;\r\n\r\n public constructor(options: InsertParticipantOptions) {\r\n this.options = options;\r\n this.description = `Insert pool ${options.participantId}`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n if (this.state !== null) {\r\n // Re-execute (redo path). Reapply mutations using saved refs.\r\n this.applyState(doc);\r\n return;\r\n }\r\n\r\n const opts = this.options;\r\n const root = opts.rootElements;\r\n\r\n // 1. Find or create a collaboration root.\r\n let collaboration = root.find(\r\n (e) => e.type === 'collaboration',\r\n ) as BpmnElement | undefined;\r\n let collaborationInsertIndex: number | null = null;\r\n if (!collaboration) {\r\n collaboration = {\r\n type: 'collaboration',\r\n id: uniqueElementId(doc, 'Collaboration'),\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n };\r\n collaborationInsertIndex = root.length;\r\n root.push(collaboration);\r\n doc.byId.set(collaboration.id, collaboration);\r\n }\r\n const collaborationWithParticipants = collaboration as BpmnElement & {\r\n participants?: BpmnElement[];\r\n };\r\n if (collaborationWithParticipants.participants === undefined) {\r\n collaborationWithParticipants.participants = [];\r\n }\r\n\r\n // 2. Reuse the existing process (wrap-on-first-pool case) or\r\n // create a new one (subsequent pools).\r\n let process: BpmnElement;\r\n let processInsertIndex: number | null = null;\r\n if (opts.existingProcess) {\r\n process = opts.existingProcess;\r\n } else {\r\n process = {\r\n type: 'process',\r\n id: opts.newProcessId,\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n };\r\n (process as BpmnElement & { flowElements?: BpmnElement[] }).flowElements = [];\r\n processInsertIndex = root.length;\r\n root.push(process);\r\n doc.byId.set(process.id, process);\r\n }\r\n\r\n // 3. Build the participant.\r\n const participant: BpmnElement = {\r\n type: 'participant',\r\n id: opts.participantId,\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n };\r\n (participant as BpmnElement & { processRef?: string }).processRef = process.id;\r\n if (opts.name !== undefined) {\r\n (participant as BpmnElement & { name?: string }).name = opts.name;\r\n }\r\n collaborationWithParticipants.participants!.push(participant);\r\n doc.byId.set(participant.id, participant);\r\n\r\n // 4. Build the initial laneSet + first lane that fills the pool.\r\n // A horizontal pool's lane axis runs left-to-right; the lane\r\n // bounds match the pool's bounds minus the 30px header strip.\r\n const HEADER = 30;\r\n const laneBounds = {\r\n x: opts.poolBounds.x + HEADER,\r\n y: opts.poolBounds.y,\r\n width: opts.poolBounds.width - HEADER,\r\n height: opts.poolBounds.height,\r\n };\r\n const initialLane: BpmnElement = {\r\n type: 'lane',\r\n id: opts.initialLaneId,\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n };\r\n (initialLane as BpmnElement & { flowNodeRef?: string[] }).flowNodeRef = [];\r\n const laneSet: BpmnElement = {\r\n type: 'laneSet',\r\n id: uniqueElementId(doc, 'LaneSet'),\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n };\r\n (laneSet as BpmnElement & { lanes?: BpmnElement[] }).lanes = [initialLane];\r\n doc.byId.set(initialLane.id, initialLane);\r\n doc.byId.set(laneSet.id, laneSet);\r\n\r\n const procWithLaneSets = process as BpmnElement & {\r\n laneSets?: BpmnElement[];\r\n };\r\n if (procWithLaneSets.laneSets === undefined) {\r\n procWithLaneSets.laneSets = [];\r\n }\r\n procWithLaneSets.laneSets.push(laneSet);\r\n\r\n // 5. Build BPMNShapes for the pool and the lane.\r\n const poolShape: BpmnElement = {\r\n type: 'BPMNShape',\r\n id: `${opts.participantId}_di`,\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n };\r\n (poolShape as BpmnElement & { bpmnElement: string }).bpmnElement = opts.participantId;\r\n (poolShape as BpmnElement & {\r\n bounds: { x: number; y: number; width: number; height: number };\r\n }).bounds = { ...opts.poolBounds };\r\n (poolShape as BpmnElement & { isHorizontal?: boolean }).isHorizontal = true;\r\n (poolShape as BpmnElement & { isExpanded?: boolean }).isExpanded = true;\r\n\r\n const laneShape: BpmnElement = {\r\n type: 'BPMNShape',\r\n id: `${opts.initialLaneId}_di`,\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n };\r\n (laneShape as BpmnElement & { bpmnElement: string }).bpmnElement = opts.initialLaneId;\r\n (laneShape as BpmnElement & {\r\n bounds: { x: number; y: number; width: number; height: number };\r\n }).bounds = { ...laneBounds };\r\n (laneShape as BpmnElement & { isHorizontal?: boolean }).isHorizontal = true;\r\n\r\n opts.planeArray.push(poolShape, laneShape);\r\n\r\n this.state = {\r\n createdCollaboration: collaborationInsertIndex !== null ? collaboration : null,\r\n collaboration,\r\n process,\r\n participant,\r\n laneSet,\r\n initialLane,\r\n poolShape,\r\n laneShape,\r\n collaborationInsertIndex,\r\n processInsertIndex,\r\n };\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n if (this.state === null) return;\r\n const s = this.state;\r\n const root = this.options.rootElements;\r\n const plane = this.options.planeArray;\r\n\r\n // Remove plane shapes.\r\n for (const sh of [s.laneShape, s.poolShape]) {\r\n const i = plane.indexOf(sh);\r\n if (i !== -1) plane.splice(i, 1);\r\n }\r\n\r\n // Remove participant from collaboration.\r\n const colWithParts = s.collaboration as BpmnElement & {\r\n participants?: BpmnElement[];\r\n };\r\n if (colWithParts.participants) {\r\n const i = colWithParts.participants.indexOf(s.participant);\r\n if (i !== -1) colWithParts.participants.splice(i, 1);\r\n }\r\n doc.byId.delete(s.participant.id);\r\n\r\n // Remove laneSet from process.\r\n const procWithLaneSets = s.process as BpmnElement & {\r\n laneSets?: BpmnElement[];\r\n };\r\n if (procWithLaneSets.laneSets) {\r\n const i = procWithLaneSets.laneSets.indexOf(s.laneSet);\r\n if (i !== -1) procWithLaneSets.laneSets.splice(i, 1);\r\n }\r\n doc.byId.delete(s.laneSet.id);\r\n doc.byId.delete(s.initialLane.id);\r\n\r\n // Remove process if this command created it.\r\n if (s.processInsertIndex !== null) {\r\n const i = root.indexOf(s.process);\r\n if (i !== -1) root.splice(i, 1);\r\n doc.byId.delete(s.process.id);\r\n }\r\n\r\n // Remove collaboration if this command created it.\r\n if (s.createdCollaboration && s.collaborationInsertIndex !== null) {\r\n const i = root.indexOf(s.collaboration);\r\n if (i !== -1) root.splice(i, 1);\r\n doc.byId.delete(s.collaboration.id);\r\n }\r\n }\r\n\r\n /** Re-apply mutations on redo using cached state. */\r\n private applyState(doc: BpmnDocument): void {\r\n if (this.state === null) return;\r\n const s = this.state;\r\n const root = this.options.rootElements;\r\n\r\n // Re-insert collaboration (if we created it) into root.\r\n if (s.createdCollaboration && s.collaborationInsertIndex !== null) {\r\n root.push(s.collaboration);\r\n doc.byId.set(s.collaboration.id, s.collaboration);\r\n }\r\n // Re-insert process (if we created it).\r\n if (s.processInsertIndex !== null) {\r\n root.push(s.process);\r\n doc.byId.set(s.process.id, s.process);\r\n }\r\n // Re-add participant.\r\n const colWithParts = s.collaboration as BpmnElement & {\r\n participants?: BpmnElement[];\r\n };\r\n if (colWithParts.participants) {\r\n colWithParts.participants.push(s.participant);\r\n }\r\n doc.byId.set(s.participant.id, s.participant);\r\n // Re-add laneSet.\r\n const procWithLaneSets = s.process as BpmnElement & {\r\n laneSets?: BpmnElement[];\r\n };\r\n if (procWithLaneSets.laneSets) {\r\n procWithLaneSets.laneSets.push(s.laneSet);\r\n }\r\n doc.byId.set(s.laneSet.id, s.laneSet);\r\n doc.byId.set(s.initialLane.id, s.initialLane);\r\n // Re-add plane shapes.\r\n this.options.planeArray.push(s.poolShape, s.laneShape);\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* DeleteLaneCommand — remove a lane and reassign its children */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Delete a lane from a laneSet, remove its BPMNShape from the plane,\r\n * and reassign every flowNodeRef it held to a sibling lane (or\r\n * \"unassigned\" when no sibling exists). Optionally stretches the\r\n * absorbing sibling lane over the freed slot so the surviving lanes\r\n * still tile the pool exactly (#8179), and optionally adjusts the\r\n * pool's own bounds.\r\n *\r\n * Per PHASE_9_PLAN §9c. Undo restores the lane, its shape, every\r\n * reassigned flowNodeRef, the sibling's bounds, and the pool's bounds.\r\n * Designed to be constructed by the facade which gathers all the\r\n * context needed.\r\n */\r\nexport class DeleteLaneCommand implements Command {\r\n public readonly kind: string = 'deleteLane';\r\n public readonly description: string;\r\n private readonly lanesArray: BpmnElement[];\r\n private readonly planeArray: BpmnElement[];\r\n private readonly removedIndex: number;\r\n private readonly lane: BpmnElement;\r\n private readonly laneShape: BpmnElement;\r\n private readonly reassignments: Array<{\r\n nodeId: string;\r\n toLaneId: string | undefined;\r\n }>;\r\n private readonly originalFlowNodeRef: string[];\r\n private readonly neighbourShape: BpmnElement | null;\r\n private readonly neighbourBoundsBefore: RectBounds | null;\r\n private readonly neighbourBoundsAfter: RectBounds | null;\r\n private readonly poolShape: BpmnElement | null;\r\n private readonly poolBoundsBefore:\r\n | { x: number; y: number; width: number; height: number }\r\n | null;\r\n private readonly poolBoundsAfter:\r\n | { x: number; y: number; width: number; height: number }\r\n | null;\r\n private executed: boolean = false;\r\n\r\n public constructor(options: {\r\n lanesArray: BpmnElement[];\r\n planeArray: BpmnElement[];\r\n removedIndex: number;\r\n lane: BpmnElement;\r\n laneShape: BpmnElement;\r\n reassignments: ReadonlyArray<{\r\n nodeId: string;\r\n toLaneId: string | undefined;\r\n }>;\r\n neighbourShape?: BpmnElement | null;\r\n neighbourBoundsBefore?: RectBounds;\r\n neighbourBoundsAfter?: RectBounds;\r\n poolShape?: BpmnElement | null;\r\n poolBoundsBefore?: { x: number; y: number; width: number; height: number };\r\n poolBoundsAfter?: { x: number; y: number; width: number; height: number };\r\n }) {\r\n this.lanesArray = options.lanesArray;\r\n this.planeArray = options.planeArray;\r\n this.removedIndex = options.removedIndex;\r\n this.lane = options.lane;\r\n this.laneShape = options.laneShape;\r\n this.reassignments = options.reassignments.map((r) => ({\r\n nodeId: r.nodeId,\r\n toLaneId: r.toLaneId,\r\n }));\r\n // Snapshot of the deleted lane's original flowNodeRef for undo.\r\n this.originalFlowNodeRef = (\r\n (options.lane as BpmnElement & { flowNodeRef?: string[] }).flowNodeRef ?? []\r\n ).slice();\r\n this.neighbourShape = options.neighbourShape ?? null;\r\n this.neighbourBoundsBefore = options.neighbourBoundsBefore\r\n ? { ...options.neighbourBoundsBefore }\r\n : null;\r\n this.neighbourBoundsAfter = options.neighbourBoundsAfter\r\n ? { ...options.neighbourBoundsAfter }\r\n : null;\r\n this.poolShape = options.poolShape ?? null;\r\n this.poolBoundsBefore = options.poolBoundsBefore\r\n ? { ...options.poolBoundsBefore }\r\n : null;\r\n this.poolBoundsAfter = options.poolBoundsAfter\r\n ? { ...options.poolBoundsAfter }\r\n : null;\r\n this.description = `Delete lane ${options.lane.id}`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n const idx = this.lanesArray.indexOf(this.lane);\r\n if (idx >= 0) this.lanesArray.splice(idx, 1);\r\n const shapeIdx = this.planeArray.indexOf(this.laneShape);\r\n if (shapeIdx >= 0) this.planeArray.splice(shapeIdx, 1);\r\n doc.byId.delete(this.lane.id);\r\n for (const ra of this.reassignments) {\r\n const node = doc.byId.get(ra.nodeId);\r\n if (!node) continue;\r\n const target = node as BpmnElement & { lane?: string };\r\n if (ra.toLaneId === undefined) {\r\n delete target.lane;\r\n } else {\r\n target.lane = ra.toLaneId;\r\n const sibling = doc.byId.get(ra.toLaneId);\r\n if (sibling) {\r\n const sib = sibling as BpmnElement & { flowNodeRef?: string[] };\r\n if (!Array.isArray(sib.flowNodeRef)) {\r\n sib.flowNodeRef = [];\r\n }\r\n if (!sib.flowNodeRef.includes(ra.nodeId)) {\r\n sib.flowNodeRef.push(ra.nodeId);\r\n }\r\n }\r\n }\r\n }\r\n if (this.neighbourShape && this.neighbourBoundsAfter) {\r\n applyBounds(this.neighbourShape, this.neighbourBoundsAfter);\r\n }\r\n if (this.poolShape && this.poolBoundsAfter) {\r\n const target = this.poolShape as BpmnElement & {\r\n bounds?: { x: number; y: number; width: number; height: number };\r\n };\r\n target.bounds = { ...this.poolBoundsAfter };\r\n }\r\n this.executed = true;\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n if (!this.executed) return;\r\n const clampedIndex = Math.max(\r\n 0,\r\n Math.min(this.removedIndex, this.lanesArray.length),\r\n );\r\n this.lanesArray.splice(clampedIndex, 0, this.lane);\r\n this.planeArray.push(this.laneShape);\r\n doc.byId.set(this.lane.id, this.lane);\r\n // Restore original flowNodeRef on the re-inserted lane.\r\n (this.lane as BpmnElement & { flowNodeRef?: string[] }).flowNodeRef =\r\n this.originalFlowNodeRef.slice();\r\n // Reverse reassignments.\r\n for (const ra of this.reassignments) {\r\n const node = doc.byId.get(ra.nodeId);\r\n if (!node) continue;\r\n const target = node as BpmnElement & { lane?: string };\r\n target.lane = this.lane.id;\r\n if (ra.toLaneId !== undefined) {\r\n const sibling = doc.byId.get(ra.toLaneId);\r\n if (sibling) {\r\n const sib = sibling as BpmnElement & { flowNodeRef?: string[] };\r\n if (Array.isArray(sib.flowNodeRef)) {\r\n const i = sib.flowNodeRef.indexOf(ra.nodeId);\r\n if (i >= 0) sib.flowNodeRef.splice(i, 1);\r\n }\r\n }\r\n }\r\n }\r\n if (this.neighbourShape && this.neighbourBoundsBefore) {\r\n applyBounds(this.neighbourShape, this.neighbourBoundsBefore);\r\n }\r\n if (this.poolShape && this.poolBoundsBefore) {\r\n const target = this.poolShape as BpmnElement & {\r\n bounds?: { x: number; y: number; width: number; height: number };\r\n };\r\n target.bounds = { ...this.poolBoundsBefore };\r\n }\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* SetLaneMembershipCommand — move a flow node between lanes */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Moves a flow node's lane membership from `previousLaneId` to\r\n * `nextLaneId`. Either id may be `undefined` — meaning \"the node was\r\n * not in any lane\" (rare: a pool with no lane set) or \"remove from\r\n * all lanes\" respectively.\r\n *\r\n * Operation: on execute, remove `elementId` from the previous lane's\r\n * `flowNodeRef` array (if any) and append it to the next lane's array\r\n * (if any). Also updates the node's `.lane` back-reference so\r\n * downstream selectors stay consistent. Undo reverses.\r\n *\r\n * Cross-pool moves are the caller's responsibility — this command\r\n * trusts that both lanes belong to the same pool. The facade enforces\r\n * the \"no cross-pool moves\" rule before constructing this command.\r\n */\r\nexport class SetLaneMembershipCommand implements Command {\r\n public readonly kind: string = 'setLaneMembership';\r\n public readonly description: string;\r\n private readonly elementId: string;\r\n private readonly previousLaneId: string | undefined;\r\n private readonly nextLaneId: string | undefined;\r\n\r\n public constructor(\r\n elementId: string,\r\n previousLaneId: string | undefined,\r\n nextLaneId: string | undefined,\r\n ) {\r\n this.elementId = elementId;\r\n this.previousLaneId = previousLaneId;\r\n this.nextLaneId = nextLaneId;\r\n this.description = `Move ${elementId} lane ${previousLaneId ?? '(none)'} -> ${nextLaneId ?? '(none)'}`;\r\n }\r\n\r\n public execute(doc: BpmnDocument): void {\r\n this.apply(doc, this.previousLaneId, this.nextLaneId);\r\n }\r\n\r\n public undo(doc: BpmnDocument): void {\r\n this.apply(doc, this.nextLaneId, this.previousLaneId);\r\n }\r\n\r\n private apply(\r\n doc: BpmnDocument,\r\n fromId: string | undefined,\r\n toId: string | undefined,\r\n ): void {\r\n if (fromId !== undefined) {\r\n const fromLane = doc.byId.get(fromId);\r\n if (fromLane) {\r\n const refs = (fromLane as BpmnElement & { flowNodeRef?: string[] })\r\n .flowNodeRef;\r\n if (Array.isArray(refs)) {\r\n const idx = refs.indexOf(this.elementId);\r\n if (idx >= 0) refs.splice(idx, 1);\r\n }\r\n }\r\n }\r\n if (toId !== undefined) {\r\n const toLane = doc.byId.get(toId);\r\n if (toLane) {\r\n const target = toLane as BpmnElement & { flowNodeRef?: string[] };\r\n if (!Array.isArray(target.flowNodeRef)) {\r\n target.flowNodeRef = [];\r\n }\r\n if (!target.flowNodeRef.includes(this.elementId)) {\r\n target.flowNodeRef.push(this.elementId);\r\n }\r\n }\r\n }\r\n const node = doc.byId.get(this.elementId);\r\n if (node) {\r\n const target = node as BpmnElement & { lane?: string };\r\n if (toId === undefined) {\r\n delete target.lane;\r\n } else {\r\n target.lane = toId;\r\n }\r\n }\r\n }\r\n}\r\n\r\n/** Visit every `BPMNShape` / `BPMNEdge` across every diagram's plane. */\r\nfunction walkPlaneElements(doc: BpmnDocument, visit: (pe: BpmnElement) => void): void {\r\n for (const diagram of doc.diagrams) {\r\n const plane = (diagram as BpmnElement & { plane?: BpmnElement & { planeElements?: BpmnElement[] } })\r\n .plane;\r\n if (!plane || !Array.isArray(plane.planeElements)) continue;\r\n for (const pe of plane.planeElements) {\r\n visit(pe);\r\n }\r\n }\r\n}\r\n","/**\r\n * mindzie.BpmnEditor — model base types\r\n * Copyright (c) mindzie 2026 — proprietary, all rights reserved\r\n *\r\n * Implements: shared interfaces for the BPMN 2.0 typed metamodel —\r\n * `BpmnElement`, `BpmnFlowElement`, `BpmnReference`, foreign-content\r\n * carriers (`ForeignNode`, `ForeignAttribute`), DI primitives (`Bounds`,\r\n * `Waypoint`, `LabelBounds`), and the root `BpmnDocument` container.\r\n * Per IMPLEMENTATION_SPEC.md Section 13.4.1.\r\n * Design influences: research-notes/phase-1.md §2, §3, §5, §6, §10\r\n * (slot lists mirror BPMN20.cs for parity with the\r\n * existing C# discovery code).\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Namespace constants */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * The five canonical XML namespace URIs the parser dispatches on.\r\n * Per research-notes/phase-1.md §1, prefixes vary wildly across exporters\r\n * (`bpmn:`, `bpmn2:`, `semantic:`, default-bound, `omgdc:`/`omgdi:`); the\r\n * URI is the only stable key.\r\n */\r\nexport const BPMN_NS = {\r\n /** Process semantics. */\r\n MODEL: 'http://www.omg.org/spec/BPMN/20100524/MODEL',\r\n /** BPMN diagram interchange (BPMNDiagram, BPMNPlane, BPMNShape, BPMNEdge). */\r\n BPMNDI: 'http://www.omg.org/spec/BPMN/20100524/DI',\r\n /** Diagram common (Bounds, Point, Font). */\r\n DC: 'http://www.omg.org/spec/DD/20100524/DC',\r\n /** Diagram interchange (Plane, Shape, Edge, waypoint). */\r\n DI: 'http://www.omg.org/spec/DD/20100524/DI',\r\n /** xsi:type discriminators. */\r\n XSI: 'http://www.w3.org/2001/XMLSchema-instance',\r\n} as const;\r\n\r\n/** Map of every BPMN namespace URI we recognise as \"ours\" (not foreign). */\r\nexport const KNOWN_BPMN_NAMESPACE_URIS: ReadonlySet = new Set([\r\n BPMN_NS.MODEL,\r\n BPMN_NS.BPMNDI,\r\n BPMN_NS.DC,\r\n BPMN_NS.DI,\r\n // Some exporters (older Camunda, Trisotech) emit MODEL under the\r\n // semantic-aliased URI. Treat as MODEL.\r\n 'http://www.omg.org/spec/BPMN/20100524/Semantic',\r\n]);\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Foreign content (extensions, vendor data) — round-trip preserves */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * A single foreign attribute on a BPMN element (e.g., `activiti:exclusive`,\r\n * `camunda:asyncBefore`). Preserved verbatim per research-notes §5.\r\n */\r\nexport interface ForeignAttribute {\r\n /** Namespace URI (never empty for a foreign attribute). */\r\n namespaceUri: string;\r\n /** Local name (no prefix). */\r\n localName: string;\r\n /** Original prefix from the source document, if known (purely cosmetic). */\r\n prefix?: string;\r\n /** String value as written in the source. */\r\n value: string;\r\n}\r\n\r\n/**\r\n * A foreign-namespace XML node preserved verbatim inside an\r\n * `` block (or as a top-level vendor decoration).\r\n * Stored as a tree of `ForeignNode` so an arbitrary-depth subtree\r\n * survives round-tripping.\r\n */\r\nexport interface ForeignNode {\r\n namespaceUri: string;\r\n localName: string;\r\n prefix?: string;\r\n attributes: ForeignAttribute[];\r\n children: ForeignNode[];\r\n /** Concatenated text content directly under this node. */\r\n text?: string;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* BPMN reference (typed wrapper around an IDREF string) */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * A typed wrapper around an IDREF string. The string is the only thing\r\n * stored — resolution to the live element happens via the document's\r\n * `byId` index (built by the parser, available to renderer/serializer).\r\n *\r\n * Phase 1 keeps references as plain `string` on the element shapes.\r\n * `BpmnReference` exists for callers who want a nominally typed marker.\r\n */\r\nexport interface BpmnReference {\r\n ref: string;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Body order — preserves child interleaving for round-trip */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Marker for a single child position inside a container, used to remember\r\n * the original interleaving of typed children and extension elements.\r\n * Per research-notes §10: Phase 2's serializer walks `bodyOrder` to produce\r\n * byte-identical output where possible.\r\n *\r\n * `kind` is the BPMN local name (or `'extensionElements'` for the\r\n * vendor-data container). `id` is the element's id (when applicable) or a\r\n * synthetic key for extension blocks.\r\n */\r\nexport interface BodyOrderEntry {\r\n kind: string;\r\n id?: string;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Common BPMN element fields */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Fields every typed BPMN element carries. The discriminator is `type`\r\n * (the BPMN local name as a string-literal union).\r\n */\r\nexport interface BpmnElement {\r\n /** Local name of the BPMN element (e.g., 'task', 'startEvent'). */\r\n type: string;\r\n /** Document-scoped XML ID. Required by the BPMN spec. */\r\n id: string;\r\n /** Optional human label. */\r\n name?: string;\r\n /** Plain-text documentation block(s) (BPMN allows multiple). */\r\n documentation?: string[];\r\n /** Foreign extension elements (vendor metadata) preserved verbatim. */\r\n extensionElements: ForeignNode[];\r\n /** Foreign attributes on this element. */\r\n foreignAttributes: ForeignAttribute[];\r\n /** Original interleaved child order (for containers). */\r\n bodyOrder?: BodyOrderEntry[];\r\n /**\r\n * `false` when the renderer treats this element as a Tier D placeholder\r\n * (see `src/model/tier-d.ts` and ELEMENT_REGISTRY.md §Tier D).\r\n * Omitted (equivalent to `true`) for Tier A/B/C elements that render\r\n * at Fidelity 2. Set by the parser in a post-parse pass so callers can\r\n * distinguish \"rendered normally\" from \"rendered as placeholder\" by\r\n * reading the element instead of re-computing the classification.\r\n */\r\n supported?: boolean;\r\n}\r\n\r\n/**\r\n * Common fields for every \"flow element\" — anything that lives inside a\r\n * `` or `` body (tasks, events, gateways, data,\r\n * artifacts, sequence flows). Per BPMN spec §10.2.3 and BPMN20.cs\r\n * `tFlowElement` / `tFlowNode`.\r\n */\r\nexport interface BpmnFlowElement extends BpmnElement {\r\n /** IDs of incoming sequence flows. */\r\n incoming?: string[];\r\n /** IDs of outgoing sequence flows. */\r\n outgoing?: string[];\r\n /** ID of the lane this node belongs to (set by lane→flowNodeRef rewrite). */\r\n lane?: string;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Diagram-Interchange primitives */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** A 2D rectangle in DI coordinate space. Coordinates may be negative. */\r\nexport interface Bounds {\r\n x: number;\r\n y: number;\r\n width: number;\r\n height: number;\r\n}\r\n\r\n/** A 2D point on an edge polyline. */\r\nexport interface Waypoint {\r\n x: number;\r\n y: number;\r\n}\r\n\r\n/**\r\n * Bounds for an attached BPMNLabel. Some exporters write label bounds\r\n * relative to the parent shape (Trisotech), others absolute — per\r\n * research-notes §4. Stored verbatim; renderer applies heuristics.\r\n */\r\nexport interface LabelBounds extends Bounds {\r\n /** Marks that the bounds were declared `0,0,0,0` (label hidden). */\r\n empty?: boolean;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Document — root container of a parsed BPMN file */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Forward declaration helpers for the discriminated unions assembled in\r\n * sibling files. We keep the document container in `base.ts` deliberately\r\n * loose (typed as `BpmnElement`) to avoid an import cycle with the\r\n * concrete element families. Sibling code narrows on `type`.\r\n */\r\nexport type AnyBpmnElement = BpmnElement;\r\n\r\n/** A processing-instruction or comment carried at the document level. */\r\nexport interface BpmnDocumentNode {\r\n kind: 'comment' | 'pi';\r\n /** For PIs: the target. For comments: undefined. */\r\n target?: string;\r\n /** Raw inner text. */\r\n data: string;\r\n /** Original sibling position relative to root-element children. */\r\n beforeRootIndex: number;\r\n}\r\n\r\n/**\r\n * Captures the original `xmlns:*` declarations that appeared on\r\n * `` so Phase 2 can re-emit identical prefix→URI bindings\r\n * (research-notes §10).\r\n */\r\nexport interface NamespaceBinding {\r\n prefix: string;\r\n uri: string;\r\n}\r\n\r\n/**\r\n * The parsed BPMN document. Fields:\r\n * - `definitionsAttributes` — the verbatim `id`, `targetNamespace`,\r\n * `exporter`, `exporterVersion`, etc. on `` (preserved\r\n * for round-trip; not type-checked further).\r\n * - `rootElements` — children of `` (processes,\r\n * collaborations, messages, errors, signals, escalations, dataStores,\r\n * itemDefinitions, imports, etc.). Typed as `BpmnElement` so this file\r\n * stays cycle-free; sibling code narrows by `type`.\r\n * - `diagrams` — every `` block, in document order.\r\n * - `byId` — an index of every element with an id. Built by the parser\r\n * for O(1) lookup at render and serialize time.\r\n * - `namespaces` — the original prefix→URI bindings from ``.\r\n * - `topLevelNodes` — comments and PIs at the document level.\r\n * - `hasDi` — `false` when no `` blocks were present\r\n * (IBM BlueWorks Live exports — research-notes §4).\r\n */\r\nexport interface BpmnDocument {\r\n /** Verbatim attributes from ``. */\r\n definitionsAttributes: Record;\r\n /** Foreign attributes on ``. */\r\n definitionsForeignAttributes: ForeignAttribute[];\r\n /** Foreign extension elements directly under ``. */\r\n definitionsExtensionElements: ForeignNode[];\r\n /** Top-level children of `` (processes, collaborations, etc.). */\r\n rootElements: BpmnElement[];\r\n /** Diagram-interchange blocks (typed `BpmnElement` for cycle-freedom). */\r\n diagrams: BpmnElement[];\r\n /** Index from element id → element for O(1) lookup. */\r\n byId: Map;\r\n /** Original namespace bindings declared on ``. */\r\n namespaces: NamespaceBinding[];\r\n /** Comments / processing instructions preserved at document level. */\r\n topLevelNodes: BpmnDocumentNode[];\r\n /** True if at least one `` was present. */\r\n hasDi: boolean;\r\n /** Original child order on `` (for serializer). */\r\n bodyOrder: BodyOrderEntry[];\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Builder helpers (used by parser; kept here for proximity to types) */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Returns a fresh, empty `BpmnDocument`. Callers populate the fields\r\n * during parsing.\r\n */\r\nexport function createEmptyDocument(): BpmnDocument {\r\n return {\r\n definitionsAttributes: {},\r\n definitionsForeignAttributes: [],\r\n definitionsExtensionElements: [],\r\n rootElements: [],\r\n diagrams: [],\r\n byId: new Map(),\r\n namespaces: [],\r\n topLevelNodes: [],\r\n hasDi: false,\r\n bodyOrder: [],\r\n };\r\n}\r\n\r\n/**\r\n * Initialises the common fields shared by every BPMN element. Kept here\r\n * (near the type) so element-family files do not duplicate the boilerplate.\r\n */\r\nexport function createBaseElement(type: T, id: string): BpmnElement {\r\n return {\r\n type,\r\n id,\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n };\r\n}\r\n\r\n/**\r\n * Returns true when the given namespace URI is one of the BPMN-owned ones\r\n * (MODEL, BPMNDI, DC, DI, or the legacy `Semantic` alias).\r\n */\r\nexport function isKnownBpmnNamespace(namespaceUri: string | undefined): boolean {\r\n if (namespaceUri === undefined || namespaceUri === '') {\r\n return false;\r\n }\r\n return KNOWN_BPMN_NAMESPACE_URIS.has(namespaceUri);\r\n}\r\n\r\n/**\r\n * The local part of a DI `bpmnElement` reference (#8267).\r\n *\r\n * `BPMNPlane.bpmnElement`, `BPMNShape.bpmnElement` and\r\n * `BPMNEdge.bpmnElement` are declared `xsd:QName` in the normative\r\n * BPMNDI schema, NOT `xsd:IDREF`. A QName is an optional prefix, a\r\n * colon, and a local part; the element it identifies is named by the\r\n * local part, because an `id` is an NCName and can never contain a\r\n * colon. Comparing the whole attribute against a set of ids therefore\r\n * reports valid prefixed references as missing - GenMyModel writes\r\n * `bpmnElement=\"bpmn2:_sSUQUx8nEeW9keBtFZy97Q\"` against\r\n * `id=\"_sSUQUx8nEeW9keBtFZy97Q\"`, and every consumer that compared the\r\n * raw string called all four of that file's healthy connectors broken.\r\n *\r\n * Lives here, in one place, because BOTH the parser's dangling-connector\r\n * report and the validator's `UNRESOLVED_DI_REF` checks need it and two\r\n * copies of a rule like this drift apart.\r\n *\r\n * The prefix is deliberately not resolved to a namespace and matched\r\n * against the target's own. These callers exist to report MISSING\r\n * targets; being stricter here would turn namespace-bookkeeping quirks\r\n * into data-loss warnings, which is the failure being fixed rather than\r\n * a fix.\r\n */\r\nexport function bpmnElementLocalPart(reference: string): string {\r\n const colon = reference.lastIndexOf(':');\r\n if (colon < 0) {\r\n return reference;\r\n }\r\n return reference.slice(colon + 1);\r\n}\r\n","/**\r\n * mindzie.BpmnEditor — artifact-family model\r\n * Copyright (c) mindzie 2026 — proprietary, all rights reserved\r\n *\r\n * Implements: typed shapes for BPMN visual artifacts — TextAnnotation\r\n * and Group. Mirrors `tTextAnnotation` and `tGroup` in BPMN20.cs.\r\n * Per IMPLEMENTATION_SPEC.md Section 13.4.1.\r\n * Design influences: research-notes/phase-1.md §2 (artifacts section),\r\n * ELEMENT_REGISTRY.md A13, C14.\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { BpmnElement } from './base.js';\r\n\r\n/**\r\n * `` (A13). Visual-only annotation; the ``\r\n * child holds the body. `textFormat` is a media-type string and defaults\r\n * to `text/plain` per the BPMN spec.\r\n */\r\nexport interface TextAnnotation extends BpmnElement {\r\n type: 'textAnnotation';\r\n /** Annotation body (verbatim). */\r\n text?: string;\r\n /** Media-type literal (e.g., 'text/plain', 'text/html'). */\r\n textFormat?: string;\r\n}\r\n\r\n/**\r\n * `` (C14). Pure visual grouping — no flow semantics. The\r\n * `categoryValueRef` points to a `` declaration that\r\n * carries the group's display label.\r\n */\r\nexport interface Group extends BpmnElement {\r\n type: 'group';\r\n categoryValueRef?: string;\r\n}\r\n\r\n/** Discriminated union of every artifact element. */\r\nexport type AnyArtifact = TextAnnotation | Group;\r\n\r\n/**\r\n * Element types BPMN files under a container's `artifacts` collection\r\n * rather than its `flowElements` (spec §8.3.4 / §10.4): the two visual\r\n * artifacts and the `` that ties them to the flow.\r\n *\r\n * `` is included because it is an artifact by the same\r\n * clause even though it has no shape of its own — `connectShapes`\r\n * already parents a new association into `artifacts`.\r\n */\r\nconst ARTIFACT_ELEMENT_TYPES: ReadonlySet = new Set([\r\n 'textAnnotation',\r\n 'group',\r\n 'association',\r\n]);\r\n\r\n/**\r\n * Whether an element of this type belongs in a container's `artifacts`\r\n * array instead of its `flowElements` array.\r\n *\r\n * This is the ONE definition of that split. It has to be shared: the\r\n * parser files an artifact correctly on read, so any authoring path that\r\n * decides the same question separately can disagree with it, and then\r\n * the SAME annotation behaves differently depending on whether it was\r\n * just drawn or was loaded from a file — Bug #8384.\r\n */\r\nexport function isArtifactType(type: string): boolean {\r\n return ARTIFACT_ELEMENT_TYPES.has(type);\r\n}\r\n","/**\r\n * mindzie.BpmnEditor — which pool does this element belong to?\r\n * Copyright (c) mindzie 2026 — proprietary, all rights reserved\r\n *\r\n * Implements: the endpoint→participant resolution that decides a\r\n * connector's type (#8186). BPMN 2.0 does not permit a Sequence Flow to\r\n * cross a Pool boundary (spec §7.5.1 / §10.4.1), so the type of a\r\n * connector is never a user preference — it follows from the endpoints:\r\n *\r\n * both endpoints in the SAME participant → sequenceFlow\r\n * endpoints in TWO DIFFERENT participants → messageFlow\r\n *\r\n * The whole rule rests on resolving the participant, and an endpoint is\r\n * rarely a direct child of the pool's process. The chain can be any of:\r\n *\r\n * task → process → participant\r\n * task partitioned into a lane → process → participant\r\n * boundary event on that task → process → participant\r\n * task in a sub-process in a\r\n * sub-process → subProcess → subProcess → process\r\n * → participant\r\n * lane / laneSet itself → laneSet chain → process → participant\r\n *\r\n * Walking that chain only ONE level (the previous behaviour) reports two\r\n * elements of the same pool as belonging to different scopes as soon as\r\n * a second level of nesting appears, and the editor then writes a\r\n * messageFlow between two shapes inside one pool. Nothing downstream\r\n * catches it: `validator.ts` only checks that the refs resolve, and no\r\n * lint rule constrains a flow to one process. A wrong answer here is\r\n * therefore silent, and it produces a BPMN-invalid file.\r\n *\r\n * So the resolution walks the container chain to the end, and it reports\r\n * `unresolved` rather than guessing whenever it cannot place BOTH\r\n * endpoints on the same footing — see {@link compareParticipantScope}.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { BpmnDocument, BpmnElement } from './base.js';\r\n\r\n/** Element types that partition a process rather than sit inside it. */\r\nconst LANE_TYPES: ReadonlySet = new Set(['lane', 'laneSet']);\r\n\r\n/**\r\n * Where an element sits in the pool structure of a document.\r\n *\r\n * `participant` is null in a pool-less document — a single ``\r\n * with no ``, which is what the editor starts from. That\r\n * is a legitimate, fully-resolved answer, not a failure: with no pools\r\n * there is no pool boundary to cross, and `process` carries the scope.\r\n */\r\nexport interface ParticipantScope {\r\n /**\r\n * The `` that owns the element. Never a sub-process — the\r\n * walk continues past every level of nesting. Null only for a\r\n * black-box participant, which has no `processRef`.\r\n */\r\n readonly process: BpmnElement | null;\r\n /** The `` (pool) drawn around that process, if any. */\r\n readonly participant: BpmnElement | null;\r\n}\r\n\r\n/**\r\n * How two resolved scopes relate.\r\n *\r\n * `unresolved` means the question could not be answered — either an\r\n * endpoint has no scope at all, or one endpoint is inside a pool and\r\n * the other is not, so \"same pool?\" has no truthful answer. Callers\r\n * must refuse the operation on `unresolved`; they must not fall back to\r\n * a default connector type, because the fallback is exactly the silent\r\n * wrong answer this module exists to prevent.\r\n */\r\nexport type ParticipantScopeRelation = 'same' | 'different' | 'unresolved';\r\n\r\n/**\r\n * Find the `` (or sub-process) that directly holds\r\n * `elementId` — in either its `flowElements` or its `artifacts` array.\r\n * Returns the INNERMOST container: for an element inside a nested\r\n * sub-process the sub-process is returned, not the outer process. Null\r\n * when the element is in neither array anywhere, or is not in the\r\n * document.\r\n *\r\n * Boundary events resolve here like any other flow element — BPMN keeps\r\n * a `` in the flowElements of the container that holds\r\n * its host activity, with the host named by `attachedToRef`, so there\r\n * is no extra hop to make.\r\n *\r\n * `artifacts` has to be searched as well as `flowElements`, and that is\r\n * the whole of Bug #8384. A text annotation or group read from a file\r\n * sits in `artifacts` (the parser is right to put it there — an\r\n * annotation is not a flow element), so a walk of `flowElements` alone\r\n * cannot place it. `resolveParticipantScope` then answers null,\r\n * `compareParticipantScope` answers `unresolved`, and `connectShapes`\r\n * refuses to draw the association — silently, because the refusal only\r\n * reaches the console. Searching both arrays is also what keeps\r\n * diagrams authored BEFORE this fix working: their annotations are in\r\n * `flowElements`, and this walk still finds them there.\r\n */\r\nexport function findContainerOf(\r\n doc: BpmnDocument,\r\n elementId: string,\r\n): BpmnElement | null {\r\n for (const root of doc.rootElements) {\r\n const found = searchContainer(root, elementId);\r\n if (found) return found;\r\n }\r\n return null;\r\n}\r\n\r\nfunction searchContainer(\r\n container: BpmnElement,\r\n elementId: string,\r\n): BpmnElement | null {\r\n const body = container as BpmnElement & {\r\n flowElements?: BpmnElement[];\r\n artifacts?: BpmnElement[];\r\n };\r\n // flowElements first: it is the far larger array in every real\r\n // diagram, and an id can only be in one of the two.\r\n for (const bucket of [body.flowElements, body.artifacts]) {\r\n if (!Array.isArray(bucket)) continue;\r\n for (const element of bucket) {\r\n if (element.id === elementId) return container;\r\n const deeper = searchContainer(element, elementId);\r\n if (deeper) return deeper;\r\n }\r\n }\r\n return null;\r\n}\r\n\r\n/**\r\n * Walk up from a container to the `` that owns it, through as\r\n * many levels of sub-process as the document has. Returns the container\r\n * itself when it is already a process, and null when the chain breaks\r\n * (a sub-process that is in no container — a malformed document).\r\n *\r\n * The loop is what makes this correct: a single hop lands on the parent\r\n * sub-process, not on the process, for anything nested twice.\r\n */\r\nexport function findOwningProcess(\r\n doc: BpmnDocument,\r\n container: BpmnElement,\r\n): BpmnElement | null {\r\n let current: BpmnElement | null = container;\r\n // Bounded by the number of elements in the document: every step moves\r\n // strictly outwards, and the guard stops a cyclic document (which the\r\n // parser should never produce) from spinning here.\r\n const guard = doc.byId.size + 1;\r\n for (let step = 0; step < guard; step += 1) {\r\n if (current === null) return null;\r\n if (current.type === 'process') return current;\r\n current = findContainerOf(doc, current.id);\r\n }\r\n return null;\r\n}\r\n\r\n/** First `` root element, or null when there is none. */\r\nexport function findCollaboration(doc: BpmnDocument): BpmnElement | null {\r\n for (const root of doc.rootElements) {\r\n if (root.type === 'collaboration') return root;\r\n }\r\n return null;\r\n}\r\n\r\n/** Every `` of every collaboration in the document. */\r\nfunction allParticipants(doc: BpmnDocument): BpmnElement[] {\r\n const result: BpmnElement[] = [];\r\n for (const root of doc.rootElements) {\r\n if (root.type !== 'collaboration') continue;\r\n const participants = (\r\n root as BpmnElement & { participants?: BpmnElement[] }\r\n ).participants;\r\n if (!Array.isArray(participants)) continue;\r\n result.push(...participants);\r\n }\r\n return result;\r\n}\r\n\r\n/**\r\n * The participant (pool) whose `processRef` names `processId`, or null\r\n * when no pool is drawn around that process.\r\n */\r\nexport function findParticipantForProcess(\r\n doc: BpmnDocument,\r\n processId: string,\r\n): BpmnElement | null {\r\n for (const participant of allParticipants(doc)) {\r\n const processRef = (participant as BpmnElement & { processRef?: string })\r\n .processRef;\r\n if (processRef === processId) return participant;\r\n }\r\n return null;\r\n}\r\n\r\n/**\r\n * The process that owns a lane or laneSet, found by searching the\r\n * `laneSets` of every process and sub-process, including nested\r\n * `childLaneSet` chains. A lane is not a flow element, so\r\n * {@link findContainerOf} cannot see it.\r\n */\r\nfunction findProcessOwningLane(\r\n doc: BpmnDocument,\r\n laneOrLaneSetId: string,\r\n): BpmnElement | null {\r\n for (const root of doc.rootElements) {\r\n const owner = searchLaneOwner(root, laneOrLaneSetId);\r\n if (owner) return findOwningProcess(doc, owner);\r\n }\r\n return null;\r\n}\r\n\r\n/**\r\n * The process or sub-process whose laneSets contain the given lane /\r\n * laneSet id, or null. Recurses into sub-processes (which carry their\r\n * own laneSets) and into `childLaneSet` for nested lanes.\r\n */\r\nfunction searchLaneOwner(\r\n container: BpmnElement,\r\n laneOrLaneSetId: string,\r\n): BpmnElement | null {\r\n const laneSets = (container as BpmnElement & { laneSets?: BpmnElement[] })\r\n .laneSets;\r\n if (Array.isArray(laneSets)) {\r\n for (const laneSet of laneSets) {\r\n if (laneSet.id === laneOrLaneSetId) return container;\r\n if (laneSetContains(laneSet, laneOrLaneSetId)) return container;\r\n }\r\n }\r\n const flowElements = (\r\n container as BpmnElement & { flowElements?: BpmnElement[] }\r\n ).flowElements;\r\n if (Array.isArray(flowElements)) {\r\n for (const element of flowElements) {\r\n const deeper = searchLaneOwner(element, laneOrLaneSetId);\r\n if (deeper) return deeper;\r\n }\r\n }\r\n return null;\r\n}\r\n\r\n/** Whether a laneSet holds the given lane / laneSet id at any depth. */\r\nfunction laneSetContains(laneSet: BpmnElement, id: string): boolean {\r\n const lanes = (laneSet as BpmnElement & { lanes?: BpmnElement[] }).lanes;\r\n if (!Array.isArray(lanes)) return false;\r\n for (const lane of lanes) {\r\n if (lane.id === id) return true;\r\n const childLaneSet = (lane as BpmnElement & { childLaneSet?: BpmnElement })\r\n .childLaneSet;\r\n if (childLaneSet === undefined) continue;\r\n if (childLaneSet.id === id) return true;\r\n if (laneSetContains(childLaneSet, id)) return true;\r\n }\r\n return false;\r\n}\r\n\r\n/**\r\n * Resolve which pool an element belongs to.\r\n *\r\n * Handles every indirection the editor can produce: a plain flow node,\r\n * a node partitioned into a lane, a boundary event attached to a task,\r\n * a node nested any number of sub-processes deep, a lane or laneSet\r\n * used as the subject, and a participant named directly.\r\n *\r\n * Returns null when the element is unknown or sits in no process and no\r\n * pool — the caller must treat that as \"cannot answer\", never as a\r\n * default.\r\n */\r\nexport function resolveParticipantScope(\r\n doc: BpmnDocument,\r\n elementId: string,\r\n): ParticipantScope | null {\r\n const element = doc.byId.get(elementId);\r\n if (!element) return null;\r\n\r\n if (element.type === 'participant') {\r\n const processRef = (element as BpmnElement & { processRef?: string })\r\n .processRef;\r\n // A black-box participant has no processRef; it is still a pool, and\r\n // reporting it as one is what lets a caller refuse deliberately.\r\n const process =\r\n processRef === undefined ? null : (doc.byId.get(processRef) ?? null);\r\n return { process, participant: element };\r\n }\r\n\r\n const process = LANE_TYPES.has(element.type)\r\n ? findProcessOwningLane(doc, elementId)\r\n : resolveProcessOfFlowElement(doc, elementId);\r\n if (!process) return null;\r\n return {\r\n process,\r\n participant: findParticipantForProcess(doc, process.id),\r\n };\r\n}\r\n\r\n/** Container walk for anything that lives in a `flowElements` array. */\r\nfunction resolveProcessOfFlowElement(\r\n doc: BpmnDocument,\r\n elementId: string,\r\n): BpmnElement | null {\r\n const container = findContainerOf(doc, elementId);\r\n if (!container) return null;\r\n return findOwningProcess(doc, container);\r\n}\r\n\r\n/**\r\n * Compare two resolved scopes.\r\n *\r\n * When both endpoints are in pools the participants decide, and the\r\n * process is irrelevant — that is the BPMN rule stated directly. When\r\n * NEITHER is in a pool the document has no pools at all and the process\r\n * decides, which is the same rule one level down (a sequence flow may\r\n * not cross a Process boundary either).\r\n *\r\n * A mixture — one endpoint inside a pool, the other outside every pool —\r\n * is reported `unresolved`. There is no honest answer to \"same pool?\"\r\n * for it, and picking one anyway is how an invalid diagram gets written\r\n * without anybody being told.\r\n */\r\nexport function compareParticipantScope(\r\n source: ParticipantScope | null,\r\n target: ParticipantScope | null,\r\n): ParticipantScopeRelation {\r\n if (source === null || target === null) return 'unresolved';\r\n if (source.participant !== null && target.participant !== null) {\r\n return source.participant.id === target.participant.id\r\n ? 'same'\r\n : 'different';\r\n }\r\n if (source.participant === null && target.participant === null) {\r\n if (source.process === null || target.process === null) {\r\n return 'unresolved';\r\n }\r\n return source.process.id === target.process.id ? 'same' : 'different';\r\n }\r\n return 'unresolved';\r\n}\r\n","/**\r\n * mindzie.BpmnEditor — Tier D classification helpers\r\n * Copyright (c) mindzie 2026 — proprietary, all rights reserved\r\n *\r\n * Implements: the `Tier D` (placeholder-rendered) classification from\r\n * ELEMENT_REGISTRY.md. Provides the authoritative set of element types /\r\n * event-definition kinds that the renderer falls back to a placeholder\r\n * for, plus helpers callers use to inspect the `supported` flag the\r\n * parser writes onto `BpmnElement`.\r\n * Per IMPLEMENTATION_SPEC.md Section 13.4 and the Phase 1 BLOCKERS.md\r\n * entry that tracked wiring `supported: false` onto Tier D nodes.\r\n * Design influences: ELEMENT_REGISTRY.md §Tier D (D1–D6). Original code.\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { BpmnElement } from './base.js';\r\n\r\n/**\r\n * Element types that render as a Tier D placeholder regardless of\r\n * their event definition.\r\n *\r\n * Historically `complexGateway` (D1) and `transaction` (D2) sat here\r\n * while their renderers fell back to a dashed-outline placeholder.\r\n * Both now render the correct BPMN vocabulary (`complexGateway` as an\r\n * asterisk-in-diamond; `transaction` as a double-bordered rounded\r\n * rectangle) via the normal gateway and subprocess family renderers.\r\n * The set is kept as infrastructure for any future type that genuinely\r\n * needs placeholder treatment; it is currently empty.\r\n */\r\nexport const TIER_D_ELEMENT_TYPES: ReadonlySet = new Set();\r\n\r\n/**\r\n * Event definition `kind` values that downgrade their containing\r\n * `endEvent` to a Tier D placeholder.\r\n *\r\n * Historically `cancel`, `error`, and `escalation` end events sat here\r\n * (D3, D4, D5 in ELEMENT_REGISTRY.md). The icon catalogue now covers\r\n * all three - end events with these definitions render their proper\r\n * glyph inside the thick end-event ring via `iconForEventDefinition`.\r\n * The set is kept as infrastructure; it is currently empty.\r\n */\r\nexport const TIER_D_END_EVENT_KINDS: ReadonlySet = new Set();\r\n\r\n/**\r\n * Narrow shape of an `endEvent` that carries an event definition. Kept\r\n * local to avoid an import cycle with `events.ts` (which imports from\r\n * `base.ts`, which is on the hot-path of every model file).\r\n */\r\ninterface EndEventLike {\r\n type: 'endEvent';\r\n eventDefinition?: { kind: string };\r\n}\r\n\r\n/**\r\n * Returns true when the element should render as a Tier D placeholder\r\n * per ELEMENT_REGISTRY.md. Does not read `element.supported`; the flag\r\n * is a post-parse cache of this predicate.\r\n */\r\nexport function isTierD(element: BpmnElement): boolean {\r\n if (TIER_D_ELEMENT_TYPES.has(element.type)) {\r\n return true;\r\n }\r\n if (element.type === 'endEvent') {\r\n const end = element as unknown as EndEventLike;\r\n const kind = end.eventDefinition?.kind;\r\n if (kind !== undefined && TIER_D_END_EVENT_KINDS.has(kind)) {\r\n return true;\r\n }\r\n }\r\n return false;\r\n}\r\n\r\n/**\r\n * Returns true when the element is supported at Fidelity 2 (Tier A/B/C).\r\n * Reads `element.supported` if present and falls back to `isTierD` when\r\n * the flag has not been set (e.g., hand-built elements in tests).\r\n */\r\nexport function isSupported(element: BpmnElement): boolean {\r\n if (element.supported === false) {\r\n return false;\r\n }\r\n if (element.supported === true) {\r\n return true;\r\n }\r\n return !isTierD(element);\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - lint shared helpers\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Shared queries used by multiple rules: type predicates, name lookup,\r\n * and a context builder that precomputes the degree maps so every rule\r\n * sees the same data shape without re-walking the document.\r\n */\r\n\r\nimport type { BpmnDocument, BpmnElement, BpmnFlowElement } from '../base.js';\r\nimport type { Process } from '../process.js';\r\nimport type { SequenceFlow } from '../flows.js';\r\nimport type { SubProcess, AdHocSubProcess } from '../activities.js';\r\nimport type { ContainerLintContext, LintContainer } from './types.js';\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Type sets */\r\n/* ------------------------------------------------------------------ */\r\n\r\nexport const FLOW_NODE_TYPES: ReadonlySet = new Set([\r\n 'startEvent',\r\n 'endEvent',\r\n 'intermediateThrowEvent',\r\n 'intermediateCatchEvent',\r\n 'boundaryEvent',\r\n 'task',\r\n 'userTask',\r\n 'serviceTask',\r\n 'sendTask',\r\n 'receiveTask',\r\n 'manualTask',\r\n 'scriptTask',\r\n 'businessRuleTask',\r\n 'callActivity',\r\n 'subProcess',\r\n 'adHocSubProcess',\r\n 'transaction',\r\n 'exclusiveGateway',\r\n 'parallelGateway',\r\n 'inclusiveGateway',\r\n 'eventBasedGateway',\r\n 'complexGateway',\r\n]);\r\n\r\nexport const GATEWAY_TYPES: ReadonlySet = new Set([\r\n 'exclusiveGateway',\r\n 'parallelGateway',\r\n 'inclusiveGateway',\r\n 'eventBasedGateway',\r\n 'complexGateway',\r\n]);\r\n\r\nexport const TASK_TYPES: ReadonlySet = new Set([\r\n 'task',\r\n 'userTask',\r\n 'serviceTask',\r\n 'sendTask',\r\n 'receiveTask',\r\n 'manualTask',\r\n 'scriptTask',\r\n 'businessRuleTask',\r\n 'callActivity',\r\n]);\r\n\r\nexport const ACTIVITY_TYPES: ReadonlySet = new Set([\r\n ...TASK_TYPES,\r\n 'subProcess',\r\n 'adHocSubProcess',\r\n 'transaction',\r\n]);\r\n\r\nexport const EVENT_TYPES: ReadonlySet = new Set([\r\n 'startEvent',\r\n 'endEvent',\r\n 'intermediateThrowEvent',\r\n 'intermediateCatchEvent',\r\n 'boundaryEvent',\r\n]);\r\n\r\nexport const SUBPROCESS_TYPES: ReadonlySet = new Set([\r\n 'subProcess',\r\n 'adHocSubProcess',\r\n 'transaction',\r\n]);\r\n\r\n/** Types that should carry a human-readable name. */\r\nexport const LABELABLE_TYPES: ReadonlySet = new Set([\r\n ...TASK_TYPES,\r\n 'subProcess',\r\n 'adHocSubProcess',\r\n 'transaction',\r\n 'intermediateThrowEvent',\r\n 'intermediateCatchEvent',\r\n 'boundaryEvent',\r\n 'exclusiveGateway',\r\n 'inclusiveGateway',\r\n 'eventBasedGateway',\r\n 'complexGateway',\r\n]);\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Predicates */\r\n/* ------------------------------------------------------------------ */\r\n\r\nexport function isFlowNode(elem: BpmnElement): elem is BpmnFlowElement {\r\n return FLOW_NODE_TYPES.has(elem.type);\r\n}\r\n\r\nexport function isGateway(type: string): boolean {\r\n return GATEWAY_TYPES.has(type);\r\n}\r\n\r\nexport function isTask(type: string): boolean {\r\n return TASK_TYPES.has(type);\r\n}\r\n\r\nexport function isActivity(type: string): boolean {\r\n return ACTIVITY_TYPES.has(type);\r\n}\r\n\r\nexport function isEvent(type: string): boolean {\r\n return EVENT_TYPES.has(type);\r\n}\r\n\r\nexport function isSubProcess(type: string): boolean {\r\n return SUBPROCESS_TYPES.has(type);\r\n}\r\n\r\nexport function isLabelable(type: string): boolean {\r\n return LABELABLE_TYPES.has(type);\r\n}\r\n\r\nexport function hasName(elem: BpmnElement): boolean {\r\n return elem.name !== undefined && elem.name.trim().length > 0;\r\n}\r\n\r\nexport function labelFor(elem: BpmnElement): string {\r\n if (elem.name !== undefined && elem.name.trim().length > 0) {\r\n return elem.name;\r\n }\r\n return elem.id;\r\n}\r\n\r\nexport function friendlyType(type: string): string {\r\n // 'userTask' -> 'User task'; 'exclusiveGateway' -> 'Exclusive gateway'.\r\n const withSpaces = type.replace(/([A-Z])/g, ' $1').trim();\r\n return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1).toLowerCase();\r\n}\r\n\r\n/**\r\n * `true` when the container is an event-subprocess (a subprocess\r\n * declared with `triggeredByEvent=\"true\"`). The start event semantics\r\n * differ from a normal subprocess.\r\n */\r\nexport function isEventSubProcess(container: LintContainer): boolean {\r\n const sp = container as SubProcess | AdHocSubProcess;\r\n return sp.type === 'subProcess' && sp.triggeredByEvent === true;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Context construction */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Builds a fully-precomputed context for one container. */\r\nexport function buildContainerContext(\r\n container: LintContainer,\r\n isTopLevel: boolean,\r\n document: BpmnDocument,\r\n): ContainerLintContext {\r\n const flowElements: BpmnElement[] =\r\n (container as { flowElements?: BpmnElement[] }).flowElements ?? [];\r\n const flowNodes = flowElements.filter(isFlowNode);\r\n const flows = flowElements.filter(\r\n (e) => e.type === 'sequenceFlow',\r\n ) as SequenceFlow[];\r\n\r\n const inCount = new Map();\r\n const outCount = new Map();\r\n const outgoingBy = new Map();\r\n const incomingBy = new Map();\r\n for (const flow of flows) {\r\n if (flow.sourceRef.length > 0) {\r\n outCount.set(flow.sourceRef, (outCount.get(flow.sourceRef) ?? 0) + 1);\r\n let list = outgoingBy.get(flow.sourceRef);\r\n if (!list) {\r\n list = [];\r\n outgoingBy.set(flow.sourceRef, list);\r\n }\r\n list.push(flow);\r\n }\r\n if (flow.targetRef.length > 0) {\r\n inCount.set(flow.targetRef, (inCount.get(flow.targetRef) ?? 0) + 1);\r\n let list = incomingBy.get(flow.targetRef);\r\n if (!list) {\r\n list = [];\r\n incomingBy.set(flow.targetRef, list);\r\n }\r\n list.push(flow);\r\n }\r\n }\r\n\r\n return {\r\n container,\r\n isTopLevel,\r\n document,\r\n flowNodes,\r\n flows,\r\n inCount,\r\n outCount,\r\n outgoingBy,\r\n incomingBy,\r\n };\r\n}\r\n\r\n/**\r\n * Walks every container in the document and yields them in document\r\n * order. Top-level processes come first, then their nested\r\n * subprocesses (depth-first).\r\n */\r\nexport function* iterateContainers(\r\n document: BpmnDocument,\r\n): Generator<{ container: LintContainer; isTopLevel: boolean }> {\r\n for (const root of document.rootElements) {\r\n if (root.type !== 'process') continue;\r\n yield { container: root as Process, isTopLevel: true };\r\n yield* iterateSubContainers(root as Process);\r\n }\r\n}\r\n\r\nfunction* iterateSubContainers(\r\n parent: LintContainer,\r\n): Generator<{ container: LintContainer; isTopLevel: boolean }> {\r\n const elements: BpmnElement[] =\r\n (parent as { flowElements?: BpmnElement[] }).flowElements ?? [];\r\n for (const child of elements) {\r\n if (isSubProcess(child.type)) {\r\n yield { container: child as LintContainer, isTopLevel: false };\r\n yield* iterateSubContainers(child as LintContainer);\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Walks every element in the document, including DI elements. Used by\r\n * the document-scoped `duplicate-id` rule.\r\n */\r\nexport function walkAll(\r\n document: BpmnDocument,\r\n visit: (elem: BpmnElement) => void,\r\n): void {\r\n for (const root of document.rootElements) walkElement(root, visit);\r\n for (const diagram of document.diagrams) walkElement(diagram, visit);\r\n}\r\n\r\nfunction walkElement(\r\n elem: BpmnElement,\r\n visit: (elem: BpmnElement) => void,\r\n): void {\r\n visit(elem);\r\n const container = elem as BpmnElement & {\r\n flowElements?: BpmnElement[];\r\n artifacts?: BpmnElement[];\r\n laneSets?: BpmnElement[];\r\n lanes?: BpmnElement[];\r\n participants?: BpmnElement[];\r\n messageFlows?: BpmnElement[];\r\n planeElements?: BpmnElement[];\r\n plane?: BpmnElement;\r\n labelStyles?: BpmnElement[];\r\n childLaneSet?: BpmnElement;\r\n };\r\n for (const slot of [\r\n container.flowElements,\r\n container.artifacts,\r\n container.laneSets,\r\n container.lanes,\r\n container.participants,\r\n container.messageFlows,\r\n container.planeElements,\r\n container.labelStyles,\r\n ]) {\r\n if (Array.isArray(slot)) {\r\n for (const child of slot) walkElement(child, visit);\r\n }\r\n }\r\n if (container.plane !== undefined) {\r\n walkElement(container.plane, visit);\r\n }\r\n if (container.childLaneSet !== undefined) {\r\n walkElement(container.childLaneSet, visit);\r\n }\r\n}\r\n","/**\r\n * Rule: start-event-required (was: no-start-event)\r\n *\r\n * Flags a container that has flow elements but no start event.\r\n * An empty container (zero flow nodes) is intentionally skipped - it's\r\n * a fresh template, not a broken process.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { labelFor } from '../helpers.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'start-event-required',\r\n defaultSeverity: 'error',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n if (ctx.flowNodes.length === 0) return [];\r\n const hasStart = ctx.flowNodes.some((e) => e.type === 'startEvent');\r\n if (hasStart) return [];\r\n const containerWord = ctx.isTopLevel ? 'Process' : 'Sub-process';\r\n const containerKey = ctx.isTopLevel ? 'process' : 'subProcess';\r\n return [\r\n {\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${containerWord} '${labelFor(ctx.container)}' has no start event.`,\r\n messageKey: 'lint.startEventRequired',\r\n messageParams: { container: containerKey, label: labelFor(ctx.container) },\r\n processId: ctx.container.id,\r\n },\r\n ];\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: end-event-required (was: no-end-event)\r\n *\r\n * Flags a container that has flow elements but no end event.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { labelFor } from '../helpers.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'end-event-required',\r\n defaultSeverity: 'error',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n if (ctx.flowNodes.length === 0) return [];\r\n const hasEnd = ctx.flowNodes.some((e) => e.type === 'endEvent');\r\n if (hasEnd) return [];\r\n const containerWord = ctx.isTopLevel ? 'Process' : 'Sub-process';\r\n const containerKey = ctx.isTopLevel ? 'process' : 'subProcess';\r\n return [\r\n {\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${containerWord} '${labelFor(ctx.container)}' has no end event.`,\r\n messageKey: 'lint.endEventRequired',\r\n messageParams: { container: containerKey, label: labelFor(ctx.container) },\r\n processId: ctx.container.id,\r\n },\r\n ];\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: single-blank-start-event\r\n *\r\n * Top-level processes should have at most ONE blank (untyped) start\r\n * event. Multiple blank starts make the entry point ambiguous - if\r\n * a process has multiple ways to start, each one should be typed\r\n * (message, signal, timer, etc.) so the trigger is explicit.\r\n *\r\n * Skipped for subprocesses - they have their own start-event rules\r\n * (sub-process-blank-start-event / event-sub-process-typed-start-event).\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { labelFor } from '../helpers.js';\r\nimport type { StartEvent } from '../../events.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'single-blank-start-event',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n if (!ctx.isTopLevel) return [];\r\n const blankStarts = ctx.flowNodes.filter(\r\n (n) =>\r\n n.type === 'startEvent' &&\r\n (n as StartEvent).eventDefinition === undefined,\r\n );\r\n if (blankStarts.length <= 1) return [];\r\n return blankStarts.map((node) => ({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Process '${labelFor(ctx.container)}' has multiple blank start events; type each entry point or merge them.`,\r\n messageKey: 'lint.singleBlankStartEvent',\r\n messageParams: { label: labelFor(ctx.container) },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n }));\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: no-disconnected (was: orphan-flow-node)\r\n *\r\n * Flow node has neither incoming nor outgoing sequence flows - it's\r\n * floating in the process body, disconnected from everything else.\r\n * Boundary events are excluded because they're attached to a host\r\n * activity and don't need their own sequence-flow connection.\r\n *\r\n * Mirrors bpmnlint's `no-disconnected` rule.\r\n */\r\n\r\nimport type {\r\n LintRule,\r\n LintIssue,\r\n ContainerLintContext,\r\n LintQuickFix,\r\n} from '../types.js';\r\nimport { friendlyType, labelFor } from '../helpers.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'no-disconnected',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (node.type === 'boundaryEvent') continue;\r\n const incoming = ctx.inCount.get(node.id) ?? 0;\r\n const outgoing = ctx.outCount.get(node.id) ?? 0;\r\n // We only fire here when BOTH sides are zero. Half-orphans are\r\n // reported by `dead-end` and `unreachable` which are more specific.\r\n const incomingExpected = node.type !== 'startEvent';\r\n const outgoingExpected = node.type !== 'endEvent';\r\n const noIncoming = incomingExpected && incoming === 0;\r\n const noOutgoing = outgoingExpected && outgoing === 0;\r\n if (noIncoming && noOutgoing) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(node.type)} '${labelFor(node)}' has no incoming or outgoing flows.`,\r\n messageKey: 'lint.noDisconnected',\r\n messageParams: { type: node.type, label: labelFor(node) },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n quickFixes(issue: LintIssue): LintQuickFix[] {\r\n const id = issue.elementId;\r\n if (!id) return [];\r\n return [\r\n {\r\n id: 'delete',\r\n label: 'Delete element',\r\n apply(editor) {\r\n editor.deleteElement(id);\r\n },\r\n },\r\n ];\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: dead-end (mindzie-specific)\r\n *\r\n * Non-end-event flow node has no outgoing sequence flow. Distinct\r\n * from `no-disconnected` because the node HAS an incoming flow -\r\n * something reaches it, but execution stops there. Boundary events\r\n * are excluded; they have their own activation model.\r\n *\r\n * Fires only when the node has at least one incoming flow; if both\r\n * sides are zero, `no-disconnected` reports the more informative\r\n * combined message.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { friendlyType, labelFor } from '../helpers.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'dead-end',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (node.type === 'boundaryEvent') continue;\r\n if (node.type === 'endEvent') continue;\r\n const incoming = ctx.inCount.get(node.id) ?? 0;\r\n const outgoing = ctx.outCount.get(node.id) ?? 0;\r\n // `no-disconnected` covers the both-zero case; here we report\r\n // only the half-orphan with at least one incoming.\r\n if (outgoing === 0 && incoming > 0) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(node.type)} '${labelFor(node)}' is a dead end (no outgoing sequence flow).`,\r\n messageKey: 'lint.deadEnd',\r\n messageParams: { type: node.type, label: labelFor(node) },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: unreachable (mindzie-specific)\r\n *\r\n * Mirror of `dead-end`: a non-start-event flow node has no incoming\r\n * sequence flow. The node has an outgoing flow (so it's not just\r\n * disconnected) but nothing reaches it.\r\n *\r\n * Start events legitimately have no incoming flow. Boundary events\r\n * are activated by their host activity, not by sequence flow, so\r\n * they're excluded too.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { friendlyType, labelFor } from '../helpers.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'unreachable',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (node.type === 'boundaryEvent') continue;\r\n if (node.type === 'startEvent') continue;\r\n const incoming = ctx.inCount.get(node.id) ?? 0;\r\n const outgoing = ctx.outCount.get(node.id) ?? 0;\r\n if (incoming === 0 && outgoing > 0) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(node.type)} '${labelFor(node)}' is unreachable (no incoming sequence flow).`,\r\n messageKey: 'lint.unreachable',\r\n messageParams: { type: node.type, label: labelFor(node) },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: dangling-flow (mindzie-specific)\r\n *\r\n * A sequence flow's sourceRef or targetRef does not resolve to an\r\n * element in the document. Almost always indicates a parse / merge\r\n * accident - the referenced shape was deleted but the flow survived.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'dangling-flow',\r\n defaultSeverity: 'error',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const flow of ctx.flows) {\r\n if (flow.sourceRef.length === 0 || !ctx.document.byId.has(flow.sourceRef)) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Sequence flow '${flow.id}' has unresolved source '${flow.sourceRef.length === 0 ? '(empty)' : flow.sourceRef}'.`,\r\n messageKey: 'lint.danglingFlow.source',\r\n messageParams: { flow: flow.id, ref: flow.sourceRef.length === 0 ? '(empty)' : flow.sourceRef },\r\n elementId: flow.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n if (flow.targetRef.length === 0 || !ctx.document.byId.has(flow.targetRef)) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Sequence flow '${flow.id}' has unresolved target '${flow.targetRef.length === 0 ? '(empty)' : flow.targetRef}'.`,\r\n messageKey: 'lint.danglingFlow.target',\r\n messageParams: { flow: flow.id, ref: flow.targetRef.length === 0 ? '(empty)' : flow.targetRef },\r\n elementId: flow.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: duplicate-id (mindzie-specific)\r\n *\r\n * Two or more elements share the same id. BPMN ids are required to\r\n * be unique across the entire document; duplicates make IDREF\r\n * resolution non-deterministic.\r\n */\r\n\r\nimport type { LintRule, LintIssue } from '../types.js';\r\nimport { walkAll } from '../helpers.js';\r\nimport type { BpmnDocument } from '../../base.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'duplicate-id',\r\n defaultSeverity: 'error',\r\n checkDocument(document: BpmnDocument): LintIssue[] {\r\n const counts = new Map();\r\n walkAll(document, (elem) => {\r\n if (elem.id.length === 0) return;\r\n counts.set(elem.id, (counts.get(elem.id) ?? 0) + 1);\r\n });\r\n const out: LintIssue[] = [];\r\n for (const [id, count] of counts.entries()) {\r\n if (count > 1) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Element id '${id}' appears ${count} times in the document.`,\r\n messageKey: 'lint.duplicateId',\r\n messageParams: { id: id, count: count },\r\n elementId: id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: no-implicit-start (bpmnlint)\r\n *\r\n * Every container with flow content should have an EXPLICIT start\r\n * event. `start-event-required` covers the missing-start case; this\r\n * rule additionally fires when there is at least one flow node that\r\n * could only be reached as an implicit start (i.e. a non-start node\r\n * with no incoming flow), even if a start event also exists. It\r\n * promotes the implicit entry point to \"make this an explicit start\r\n * event\" feedback.\r\n *\r\n * Why separate from `unreachable`? `unreachable` reports any node\r\n * with zero incoming. `no-implicit-start` is the *modeling style*\r\n * version: the start is implicit and that's the design smell.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { friendlyType, labelFor } from '../helpers.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'no-implicit-start',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n // If the container has no flow content at all, there's nothing\r\n // to be implicit about.\r\n if (ctx.flowNodes.length === 0) return [];\r\n // If the container has zero start events AND has flow content,\r\n // `start-event-required` already reports the issue. Don't\r\n // double-flag.\r\n const hasExplicitStart = ctx.flowNodes.some((e) => e.type === 'startEvent');\r\n if (!hasExplicitStart) return [];\r\n // With an explicit start present, any OTHER node that has zero\r\n // incoming flow is acting as an implicit start (it could only be\r\n // reached by starting execution there).\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (node.type === 'startEvent') continue;\r\n if (node.type === 'boundaryEvent') continue;\r\n const incoming = ctx.inCount.get(node.id) ?? 0;\r\n const outgoing = ctx.outCount.get(node.id) ?? 0;\r\n // Pure orphans are handled by `no-disconnected`; we want only\r\n // outbound-only nodes here.\r\n if (incoming === 0 && outgoing > 0) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(node.type)} '${labelFor(node)}' acts as an implicit start; add an explicit start event upstream.`,\r\n messageKey: 'lint.noImplicitStart',\r\n messageParams: { type: node.type, label: labelFor(node) },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: no-implicit-end (bpmnlint)\r\n *\r\n * Mirror of `no-implicit-start`: when an explicit end event exists\r\n * but there's also a node with zero outgoing flow that isn't an end\r\n * event, that node is acting as an implicit end.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { friendlyType, labelFor } from '../helpers.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'no-implicit-end',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n if (ctx.flowNodes.length === 0) return [];\r\n const hasExplicitEnd = ctx.flowNodes.some((e) => e.type === 'endEvent');\r\n if (!hasExplicitEnd) return [];\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (node.type === 'endEvent') continue;\r\n if (node.type === 'boundaryEvent') continue;\r\n const incoming = ctx.inCount.get(node.id) ?? 0;\r\n const outgoing = ctx.outCount.get(node.id) ?? 0;\r\n if (outgoing === 0 && incoming > 0) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(node.type)} '${labelFor(node)}' acts as an implicit end; add an explicit end event downstream.`,\r\n messageKey: 'lint.noImplicitEnd',\r\n messageParams: { type: node.type, label: labelFor(node) },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: no-implicit-split (bpmnlint)\r\n *\r\n * A task or event with > 1 outgoing sequence flow creates an\r\n * implicit parallel split. BPMN allows it but it hides the\r\n * branching semantics - readers can't tell at a glance whether the\r\n * fork is parallel, exclusive, or conditional. The fix is to add an\r\n * explicit gateway in front of the diverging flows.\r\n *\r\n * Gateways legitimately have multiple outgoing flows and are skipped.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { friendlyType, isGateway, labelFor } from '../helpers.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'no-implicit-split',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (isGateway(node.type)) continue;\r\n // Start events legitimately have outgoing but no incoming; we\r\n // still flag them if they fan out to multiple successors\r\n // because the split is semantically the same problem.\r\n const outgoing = ctx.outCount.get(node.id) ?? 0;\r\n if (outgoing > 1) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(node.type)} '${labelFor(node)}' has ${outgoing} outgoing flows (implicit split); use an explicit gateway.`,\r\n messageKey: 'lint.noImplicitSplit',\r\n messageParams: { type: node.type, label: labelFor(node), count: outgoing },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: fake-join (bpmnlint)\r\n *\r\n * A task or event with > 1 incoming sequence flow creates an implicit\r\n * join. Per BPMN semantics this is a \"merge\" (any incoming token\r\n * activates the node) but readers may expect a synchronising parallel\r\n * join instead. The fix is to add an explicit joining gateway -\r\n * exclusive for merge, parallel for synchronisation.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { friendlyType, isGateway, labelFor } from '../helpers.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'fake-join',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (isGateway(node.type)) continue;\r\n if (node.type === 'startEvent') continue;\r\n const incoming = ctx.inCount.get(node.id) ?? 0;\r\n if (incoming > 1) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(node.type)} '${labelFor(node)}' has ${incoming} incoming flows (fake join); use an explicit joining gateway.`,\r\n messageKey: 'lint.fakeJoin',\r\n messageParams: { type: node.type, label: labelFor(node), count: incoming },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: no-gateway-join-fork (bpmnlint)\r\n *\r\n * A single gateway should NOT both join (>1 incoming) and fork (>1\r\n * outgoing). The semantics depend on the gateway type and are\r\n * frequently surprising to readers; clearer modeling uses two\r\n * gateways - one to join, one to fork.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { friendlyType, isGateway, labelFor } from '../helpers.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'no-gateway-join-fork',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (!isGateway(node.type)) continue;\r\n const incoming = ctx.inCount.get(node.id) ?? 0;\r\n const outgoing = ctx.outCount.get(node.id) ?? 0;\r\n if (incoming > 1 && outgoing > 1) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(node.type)} '${labelFor(node)}' both joins (${incoming} in) and forks (${outgoing} out); split it into two gateways.`,\r\n messageKey: 'lint.noGatewayJoinFork',\r\n messageParams: { type: node.type, label: labelFor(node), incoming: incoming, outgoing: outgoing },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: superfluous-gateway (bpmnlint)\r\n *\r\n * A gateway with exactly one incoming and one outgoing flow does\r\n * nothing - it neither merges nor branches. The flow can pass\r\n * directly between source and target.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext, LintQuickFix } from '../types.js';\r\nimport { friendlyType, isGateway, labelFor } from '../helpers.js';\r\nimport type { BpmnDocument } from '../../base.js';\r\nimport type { SequenceFlow } from '../../flows.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'superfluous-gateway',\r\n defaultSeverity: 'info',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (!isGateway(node.type)) continue;\r\n const incoming = ctx.inCount.get(node.id) ?? 0;\r\n const outgoing = ctx.outCount.get(node.id) ?? 0;\r\n if (incoming === 1 && outgoing === 1) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(node.type)} '${labelFor(node)}' has one in and one out - it neither joins nor forks and can be removed.`,\r\n messageKey: 'lint.superfluousGateway',\r\n messageParams: { type: node.type, label: labelFor(node) },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n quickFixes(issue: LintIssue, document: BpmnDocument): LintQuickFix[] {\r\n const gatewayId = issue.elementId;\r\n if (!gatewayId) return [];\r\n // Find the gateway's unique source and target flow neighbours.\r\n let inFlow: SequenceFlow | null = null;\r\n let outFlow: SequenceFlow | null = null;\r\n for (const el of document.byId.values()) {\r\n if (el.type !== 'sequenceFlow') continue;\r\n const flow = el as SequenceFlow;\r\n if (flow.targetRef === gatewayId) inFlow = flow;\r\n if (flow.sourceRef === gatewayId) outFlow = flow;\r\n }\r\n if (!inFlow || !outFlow) return [];\r\n const sourceId = inFlow.sourceRef;\r\n const targetId = outFlow.targetRef;\r\n return [\r\n {\r\n id: 'bypass-and-delete',\r\n label: 'Remove gateway',\r\n apply(editor) {\r\n // Delete the gateway (this also drops both adjacent flows\r\n // through deleteElement's cascade), then reconnect source\r\n // directly to target.\r\n editor.deleteElement(gatewayId);\r\n editor.connectShapes(sourceId, targetId);\r\n },\r\n },\r\n ];\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: superfluous-termination (bpmnlint)\r\n *\r\n * A terminate end event force-cancels all parallel activity in the\r\n * process. If a process has BOTH a terminate end event AND at least\r\n * one regular end event, the regular end is reachable only when no\r\n * terminate end has fired - making it harder to reason about. One\r\n * terminate end is usually enough.\r\n *\r\n * Heuristic: if a container has any terminate end event AND more\r\n * than one end event total, flag the extra terminate-ends (the first\r\n * is the \"primary\"; additional terminate-ends are usually redundant\r\n * because any one terminate cancels everything).\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { labelFor } from '../helpers.js';\r\nimport type { EndEvent } from '../../events.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'superfluous-termination',\r\n defaultSeverity: 'info',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const terminateEnds = ctx.flowNodes.filter(\r\n (n) =>\r\n n.type === 'endEvent' &&\r\n (n as EndEvent).eventDefinition?.kind === 'terminate',\r\n );\r\n if (terminateEnds.length <= 1) return [];\r\n // Skip the first; flag the rest.\r\n return terminateEnds.slice(1).map((node) => ({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Terminate end event '${labelFor(node)}' is redundant - one terminate end cancels all parallel activity.`,\r\n messageKey: 'lint.superfluousTermination',\r\n messageParams: { label: labelFor(node) },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n }));\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: no-duplicate-sequence-flows (bpmnlint)\r\n *\r\n * Two sequence flows that share the same source and target are\r\n * always a mistake - they add no execution semantics and clutter\r\n * the diagram. Frequently the result of accidentally drawing a\r\n * second connection between already-connected shapes.\r\n */\r\n\r\nimport type {\r\n LintRule,\r\n LintIssue,\r\n ContainerLintContext,\r\n LintQuickFix,\r\n} from '../types.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'no-duplicate-sequence-flows',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const seen = new Map();\r\n const out: LintIssue[] = [];\r\n for (const flow of ctx.flows) {\r\n const key = `${flow.sourceRef}->${flow.targetRef}`;\r\n const first = seen.get(key);\r\n if (first === undefined) {\r\n seen.set(key, flow.id);\r\n continue;\r\n }\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Sequence flow '${flow.id}' duplicates flow '${first}' (same source and target).`,\r\n messageKey: 'lint.noDuplicateSequenceFlows',\r\n messageParams: { flow: flow.id, first: first },\r\n elementId: flow.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n return out;\r\n },\r\n quickFixes(issue: LintIssue): LintQuickFix[] {\r\n const id = issue.elementId;\r\n if (!id) return [];\r\n return [\r\n {\r\n id: 'delete-duplicate',\r\n label: 'Delete duplicate flow',\r\n apply(editor) {\r\n editor.deleteElement(id);\r\n },\r\n },\r\n ];\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: no-complex-gateway (bpmnlint)\r\n *\r\n * Complex gateways carry a free-form `activationCondition`\r\n * expression and are notoriously hard to reason about. Most flows\r\n * can be modelled with simpler gateways. Flag every complex\r\n * gateway for review.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { labelFor } from '../helpers.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'no-complex-gateway',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n return ctx.flowNodes\r\n .filter((n) => n.type === 'complexGateway')\r\n .map((node) => ({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Complex gateway '${labelFor(node)}' is hard to reason about; prefer exclusive, parallel, or inclusive gateways.`,\r\n messageKey: 'lint.noComplexGateway',\r\n messageParams: { label: labelFor(node) },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n }));\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: no-inclusive-gateway (bpmnlint - opinionated)\r\n *\r\n * OR splits/joins are valid BPMN but harder to reason about than\r\n * XOR or parallel because more than one (but not necessarily all)\r\n * branches may activate. Many style guides discourage them.\r\n *\r\n * Severity is `info` and off by default in the recommended preset -\r\n * many domains legitimately need OR semantics. Enable when you want\r\n * a strict style.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { labelFor } from '../helpers.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'no-inclusive-gateway',\r\n defaultSeverity: 'info',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n return ctx.flowNodes\r\n .filter((n) => n.type === 'inclusiveGateway')\r\n .map((node) => ({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Inclusive gateway '${labelFor(node)}' (OR semantics) is harder to reason about; consider exclusive or parallel.`,\r\n messageKey: 'lint.noInclusiveGateway',\r\n messageParams: { label: labelFor(node) },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n }));\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: event-based-gateway (bpmnlint)\r\n *\r\n * An event-based gateway must be followed only by intermediate\r\n * catch events (or receive tasks). Anything else - a regular task,\r\n * a throwing event, another gateway - violates the gateway's\r\n * \"race between events\" semantics.\r\n *\r\n * Allowed target types per BPMN spec:\r\n * - intermediateCatchEvent (the common case)\r\n * - receiveTask\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { friendlyType, labelFor } from '../helpers.js';\r\n\r\nconst ALLOWED_TARGETS: ReadonlySet = new Set([\r\n 'intermediateCatchEvent',\r\n 'receiveTask',\r\n]);\r\n\r\nconst rule: LintRule = {\r\n id: 'event-based-gateway',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (node.type !== 'eventBasedGateway') continue;\r\n const outgoing = ctx.outgoingBy.get(node.id) ?? [];\r\n for (const flow of outgoing) {\r\n const target = ctx.document.byId.get(flow.targetRef);\r\n if (!target) continue;\r\n if (!ALLOWED_TARGETS.has(target.type)) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Event-based gateway '${labelFor(node)}' has a successor of type ${friendlyType(target.type)}; only intermediate catch events and receive tasks are allowed.`,\r\n messageKey: 'lint.eventBasedGateway',\r\n messageParams: { label: labelFor(node), type: target.type },\r\n elementId: target.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: sub-process-blank-start-event (bpmnlint)\r\n *\r\n * An embedded sub-process must use a BLANK (untyped) start event.\r\n * Typed start events are reserved for event sub-processes (the\r\n * sibling rule `event-sub-process-typed-start-event` enforces that\r\n * direction).\r\n *\r\n * Applies only to subprocesses that are NOT event-triggered.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { isEventSubProcess, labelFor } from '../helpers.js';\r\nimport type { StartEvent } from '../../events.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'sub-process-blank-start-event',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n if (ctx.isTopLevel) return [];\r\n if (isEventSubProcess(ctx.container)) return [];\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (node.type !== 'startEvent') continue;\r\n const ev = node as StartEvent;\r\n if (ev.eventDefinition !== undefined) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Start event '${labelFor(node)}' inside sub-process '${labelFor(ctx.container)}' is typed; embedded sub-processes must use blank start events.`,\r\n messageKey: 'lint.subProcessBlankStartEvent',\r\n messageParams: { label: labelFor(node), container: labelFor(ctx.container) },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: event-sub-process-typed-start-event (bpmnlint)\r\n *\r\n * An event sub-process (`triggeredByEvent=\"true\"`) must use a TYPED\r\n * start event (message / timer / signal / error / escalation /\r\n * compensation / conditional). A blank start makes no sense - the\r\n * event sub-process can never trigger.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { isEventSubProcess, labelFor } from '../helpers.js';\r\nimport type { StartEvent } from '../../events.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'event-sub-process-typed-start-event',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n if (ctx.isTopLevel) return [];\r\n if (!isEventSubProcess(ctx.container)) return [];\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (node.type !== 'startEvent') continue;\r\n const ev = node as StartEvent;\r\n if (ev.eventDefinition === undefined) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Start event '${labelFor(node)}' in event sub-process '${labelFor(ctx.container)}' is blank; event sub-processes require a typed start.`,\r\n messageKey: 'lint.eventSubProcessTypedStartEvent',\r\n messageParams: { label: labelFor(node), container: labelFor(ctx.container) },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: conditional-flows (bpmnlint)\r\n *\r\n * When an exclusive or inclusive gateway has any conditional\r\n * outgoing flow OR a default flow declared, EVERY non-default\r\n * outgoing flow must declare a condition. Mixing conditional and\r\n * non-conditional outgoing flows from the same fork creates\r\n * ambiguous routing semantics.\r\n *\r\n * Parallel gateways are skipped - they activate all branches\r\n * unconditionally and conditions on parallel branches are ignored\r\n * by the BPMN execution model.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { labelFor } from '../helpers.js';\r\nimport type { AnyGateway } from '../../gateways.js';\r\n\r\nconst CONDITIONAL_GATEWAY_TYPES: ReadonlySet = new Set([\r\n 'exclusiveGateway',\r\n 'inclusiveGateway',\r\n 'complexGateway',\r\n]);\r\n\r\nconst rule: LintRule = {\r\n id: 'conditional-flows',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (!CONDITIONAL_GATEWAY_TYPES.has(node.type)) continue;\r\n const gw = node as AnyGateway;\r\n const outgoing = ctx.outgoingBy.get(node.id) ?? [];\r\n if (outgoing.length < 2) continue;\r\n const defaultRef = gw.default;\r\n const hasAnyCondition = outgoing.some(\r\n (f) => f.conditionExpression !== undefined && f.conditionExpression.length > 0,\r\n );\r\n const hasDefault = defaultRef !== undefined && defaultRef.length > 0;\r\n // Trigger when the fork is \"conditional\" (any condition or\r\n // a default flow exists) - then EVERY non-default branch must\r\n // declare a condition.\r\n if (!hasAnyCondition && !hasDefault) continue;\r\n for (const flow of outgoing) {\r\n if (flow.id === defaultRef) continue;\r\n const hasCondition =\r\n flow.conditionExpression !== undefined &&\r\n flow.conditionExpression.length > 0;\r\n if (!hasCondition) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Outgoing flow '${flow.id}' from ${labelFor(node)} needs a condition (sibling flows are conditional).`,\r\n messageKey: 'lint.conditionalFlows',\r\n messageParams: { flow: flow.id, label: labelFor(node) },\r\n elementId: flow.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: label-required (was: missing-label)\r\n *\r\n * Activities, sub-processes, gateways, and intermediate / boundary\r\n * events should carry a human-readable name. Start and end events\r\n * may legitimately be blank (icon-only).\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { friendlyType, hasName, isLabelable } from '../helpers.js';\r\n\r\nconst rule: LintRule = {\r\n id: 'label-required',\r\n defaultSeverity: 'info',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (!isLabelable(node.type)) continue;\r\n if (!hasName(node)) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(node.type)} '${node.id}' has no name.`,\r\n messageKey: 'lint.labelRequired',\r\n messageParams: { type: node.type, id: node.id },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: gateway-mismatch (mindzie-specific)\r\n *\r\n * When a gateway carries an explicit `gatewayDirection` attribute,\r\n * its actual topology should match the declared direction. Phase 12\r\n * also infers direction from topology when the attribute is\r\n * `Unspecified` so a gateway with 1 in / 2 out is treated as\r\n * `Diverging` for the mismatch check.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { friendlyType, isGateway, labelFor } from '../helpers.js';\r\nimport type { AnyGateway, GatewayDirection } from '../../gateways.js';\r\n\r\nfunction inferDirection(ins: number, outs: number): GatewayDirection {\r\n if (ins > 1 && outs > 1) return 'Mixed';\r\n if (outs > 1) return 'Diverging';\r\n if (ins > 1) return 'Converging';\r\n return 'Unspecified';\r\n}\r\n\r\nconst rule: LintRule = {\r\n id: 'gateway-mismatch',\r\n defaultSeverity: 'warning',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (!isGateway(node.type)) continue;\r\n const gw = node as AnyGateway;\r\n const ins = ctx.inCount.get(gw.id) ?? 0;\r\n const outs = ctx.outCount.get(gw.id) ?? 0;\r\n const declared = gw.gatewayDirection;\r\n if (declared === undefined || declared === 'Unspecified') continue;\r\n const actual = inferDirection(ins, outs);\r\n if (declared === actual) continue;\r\n if (declared === 'Diverging' && outs < 2) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(gw.type)} '${labelFor(gw)}' is declared Diverging but has ${outs} outgoing flow(s).`,\r\n messageKey: 'lint.gatewayMismatch.diverging',\r\n messageParams: { type: gw.type, label: labelFor(gw), direction: 'diverging', count: outs },\r\n elementId: gw.id,\r\n processId: ctx.container.id,\r\n });\r\n continue;\r\n }\r\n if (declared === 'Converging' && ins < 2) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(gw.type)} '${labelFor(gw)}' is declared Converging but has ${ins} incoming flow(s).`,\r\n messageKey: 'lint.gatewayMismatch.converging',\r\n messageParams: { type: gw.type, label: labelFor(gw), direction: 'converging', count: ins },\r\n elementId: gw.id,\r\n processId: ctx.container.id,\r\n });\r\n continue;\r\n }\r\n if (declared === 'Mixed' && (ins < 2 || outs < 2)) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(gw.type)} '${labelFor(gw)}' is declared Mixed but has ${ins} incoming / ${outs} outgoing flow(s).`,\r\n messageKey: 'lint.gatewayMismatch.mixed',\r\n messageParams: { type: gw.type, label: labelFor(gw), direction: 'mixed', incoming: ins, outgoing: outs },\r\n elementId: gw.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: verb-object-activity-label (Phase 18 - 7PMG G6)\r\n *\r\n * Activities (tasks, sub-processes) should be named with a verb-object\r\n * phrase like \"Approve invoice\", \"Send notification\". Discouraged forms\r\n * include noun-only (\"Invoice approval\"), gerund/passive (\"Approving\r\n * the invoice\"), or empty.\r\n *\r\n * Heuristic: the label must start with a base-form verb. We detect the\r\n * common anti-patterns rather than try to validate the verb dictionary:\r\n *\r\n * - Empty / whitespace-only label - handled by label-required, skip\r\n * - Ends in 'ing' or 'ion' (likely a noun / gerund: \"Approval\",\r\n * \"Processing\", \"Reconciliation\")\r\n * - First word ends in 'ed' (passive: \"Approved\", \"Sent\")\r\n * - First word is \"to\" (infinitive: \"To approve\")\r\n * - First word is shorter than 2 chars (probably not a verb)\r\n *\r\n * Off by default in the recommended preset - the heuristic is imperfect\r\n * and some valid English verbs end in 'ing'/'ion' (e.g. \"Sign\", \"Run\"\r\n * are fine, but a noun like \"Cleaning\" / \"Discussion\" trips this).\r\n *\r\n * Reference: Mendling, Reijers, van der Aalst (2010), 7PMG guideline 6.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { isActivity, labelFor } from '../helpers.js';\r\n\r\n/** Word endings that strongly indicate a noun (derived from a verb). */\r\nconst NOUNY_SUFFIXES: readonly string[] = [\r\n 'ion', // approval, reconciliation, decision\r\n 'ment', // management, agreement, judgment\r\n 'ance', // acceptance, compliance, attendance\r\n 'ence', // reference, dependence\r\n 'ity', // validity, security\r\n 'ness', // readiness, completeness\r\n 'ship', // ownership, partnership\r\n 'al', // approval, refusal, dismissal, reversal\r\n];\r\nconst PASSIVE_SUFFIX = 'ed';\r\nconst GERUND_SUFFIX = 'ing';\r\n\r\nfunction endsWithNounSuffix(word: string): boolean {\r\n for (const sfx of NOUNY_SUFFIXES) {\r\n if (word.length > sfx.length + 2 && word.endsWith(sfx)) return true;\r\n }\r\n return false;\r\n}\r\n\r\nfunction looksLikeVerbObject(label: string): boolean {\r\n const trimmed = label.trim();\r\n if (trimmed.length < 3) return false;\r\n const words = trimmed.split(/\\s+/);\r\n const firstWord = words[0]!.toLowerCase();\r\n if (firstWord.length < 2) return false;\r\n if (firstWord === 'to') return false; // infinitive\r\n // Ends in passive 'ed' but not a known irregular verb (\"Send\", \"Read\")\r\n // - \"Approved\", \"Sent\" (no), \"Read\" (kept, 4 chars).\r\n if (firstWord.length > 3 && firstWord.endsWith(PASSIVE_SUFFIX)) return false;\r\n // Gerund / -ing (\"Approving\", \"Processing\"). Most legitimate verbs\r\n // are < 5 chars when they end in -ing as a present-tense form\r\n // (\"Sing\", \"Ring\"). Treat anything 7+ ending in -ing as gerund.\r\n if (firstWord.length >= 7 && firstWord.endsWith(GERUND_SUFFIX)) return false;\r\n // First word is itself a noun-form (\"Approval\", \"Reconciliation\")\r\n if (endsWithNounSuffix(firstWord)) return false;\r\n // Any other word in the phrase ends with a noun suffix - typical\r\n // \"X approval / X reconciliation\" noun-phrase pattern. Catches\r\n // \"Invoice approval\" (first word OK as a verb, but second word\r\n // betrays the noun-phrase intent).\r\n for (let i = 1; i < words.length; i += 1) {\r\n const w = words[i]!.toLowerCase().replace(/[.,!?;:]+$/g, '');\r\n if (endsWithNounSuffix(w)) return false;\r\n }\r\n return true;\r\n}\r\n\r\nconst rule: LintRule = {\r\n id: 'verb-object-activity-label',\r\n defaultSeverity: 'info',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (!isActivity(node.type)) continue;\r\n const name = node.name;\r\n if (name === undefined || name.trim().length === 0) continue;\r\n if (looksLikeVerbObject(name)) continue;\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Activity '${labelFor(node)}' should start with an action verb (e.g. \"Approve invoice\", \"Send notification\").`,\r\n messageKey: 'lint.verbObjectActivityLabel',\r\n messageParams: { label: labelFor(node) },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: past-participle-event-label (Phase 18 - convention)\r\n *\r\n * Events should be labelled as completed states / occurrences:\r\n * - \"Invoice received\" (good - past participle)\r\n * - \"Receive invoice\" (bad - verb phrase, this is what tasks do)\r\n *\r\n * Heuristic: event label's last word ends in 'ed' or 'en' (past\r\n * participle) OR matches a known irregular past participle\r\n * (\"received\", \"sent\", \"completed\", \"approved\", \"rejected\" etc.).\r\n *\r\n * Boundary events get a free pass - they're often labelled by the\r\n * triggering event name itself (\"Timer\", \"Error 500\") without a\r\n * verb component.\r\n *\r\n * Reference: BPMN style guides (Trisotech, Camunda Best Practices,\r\n * Bruce Silver's Method & Style).\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { labelFor } from '../helpers.js';\r\n\r\nconst EVENT_TYPES_TO_CHECK: ReadonlySet = new Set([\r\n 'startEvent',\r\n 'endEvent',\r\n 'intermediateThrowEvent',\r\n 'intermediateCatchEvent',\r\n]);\r\n\r\nconst IRREGULAR_PAST_PARTICIPLES: ReadonlySet = new Set([\r\n 'sent',\r\n 'made',\r\n 'gone',\r\n 'done',\r\n 'taken',\r\n 'given',\r\n 'known',\r\n 'shown',\r\n 'seen',\r\n 'paid',\r\n 'put',\r\n 'set',\r\n 'cut',\r\n 'hit',\r\n 'read',\r\n 'come',\r\n 'run',\r\n 'lost',\r\n 'found',\r\n 'won',\r\n 'bought',\r\n 'sold',\r\n 'caught',\r\n 'taught',\r\n 'brought',\r\n 'thought',\r\n 'kept',\r\n 'left',\r\n 'met',\r\n 'told',\r\n 'held',\r\n 'felt',\r\n 'built',\r\n]);\r\n\r\nfunction looksLikePastParticiple(label: string): boolean {\r\n const trimmed = label.trim();\r\n if (trimmed.length === 0) return true; // empty = label-required handles it\r\n const words = trimmed.split(/\\s+/);\r\n const last = words[words.length - 1]!.toLowerCase().replace(/[.!?,;:]+$/g, '');\r\n if (last.length < 3) return true; // single-letter trailing words, skip\r\n if (last.endsWith('ed')) return true;\r\n if (last.endsWith('en') && last.length >= 5) return true;\r\n if (IRREGULAR_PAST_PARTICIPLES.has(last)) return true;\r\n return false;\r\n}\r\n\r\nconst rule: LintRule = {\r\n id: 'past-participle-event-label',\r\n defaultSeverity: 'info',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n if (!EVENT_TYPES_TO_CHECK.has(node.type)) continue;\r\n const name = node.name;\r\n if (name === undefined || name.trim().length === 0) continue;\r\n if (looksLikePastParticiple(name)) continue;\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Event '${labelFor(node)}' should be labelled as a completed state (e.g. \"Invoice received\", \"Order shipped\").`,\r\n messageKey: 'lint.pastParticipleEventLabel',\r\n messageParams: { label: labelFor(node) },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: decompose-large-process (Phase 18 - 7PMG G7)\r\n *\r\n * A top-level process with more than 50 flow nodes is harder for\r\n * readers to grasp. The 7PMG guideline suggests decomposing into\r\n * sub-processes to keep each level cognitively manageable.\r\n *\r\n * Threshold: 50 elements is the published 7PMG figure. Adjustable via\r\n * an environment-level rule-option in a later phase if needed.\r\n *\r\n * Counts ONLY flow nodes (tasks, events, gateways, subprocesses) -\r\n * not sequence flows, lanes, or DI. Subprocess bodies don't add to\r\n * the parent's count.\r\n *\r\n * Reference: Mendling, Reijers, van der Aalst (2010), 7PMG guideline 7.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { labelFor } from '../helpers.js';\r\n\r\nconst THRESHOLD = 50;\r\n\r\nconst rule: LintRule = {\r\n id: 'decompose-large-process',\r\n defaultSeverity: 'info',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n if (!ctx.isTopLevel) return [];\r\n const count = ctx.flowNodes.length;\r\n if (count <= THRESHOLD) return [];\r\n return [\r\n {\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `Process '${labelFor(ctx.container)}' has ${count} flow nodes (threshold ${THRESHOLD}); consider decomposing into sub-processes.`,\r\n messageKey: 'lint.decomposeLargeProcess',\r\n messageParams: { label: labelFor(ctx.container), count: count, threshold: THRESHOLD },\r\n processId: ctx.container.id,\r\n },\r\n ];\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * Rule: minimize-routing-paths (Phase 18 - 7PMG G2)\r\n *\r\n * Flow nodes with too many adjacent flows are hard to read. The 7PMG\r\n * guideline says \"minimize the routing paths per element\" - a node\r\n * with > 5 incoming or > 5 outgoing flows is a candidate for\r\n * splitting into multiple gateways / sub-paths.\r\n *\r\n * Threshold: 5. Same heuristic Camunda's Best Practices Modeling guide\r\n * recommends. Gateways are not exempt - even a 6-way exclusive split\r\n * is hard to read; consider hierarchical decision tables instead.\r\n *\r\n * Reference: Mendling, Reijers, van der Aalst (2010), 7PMG guideline 2.\r\n */\r\n\r\nimport type { LintRule, LintIssue, ContainerLintContext } from '../types.js';\r\nimport { friendlyType, labelFor } from '../helpers.js';\r\n\r\nconst MAX_PATHS = 5;\r\n\r\nconst rule: LintRule = {\r\n id: 'minimize-routing-paths',\r\n defaultSeverity: 'info',\r\n checkContainer(ctx: ContainerLintContext): LintIssue[] {\r\n const out: LintIssue[] = [];\r\n for (const node of ctx.flowNodes) {\r\n const incoming = ctx.inCount.get(node.id) ?? 0;\r\n const outgoing = ctx.outCount.get(node.id) ?? 0;\r\n if (outgoing > MAX_PATHS) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(node.type)} '${labelFor(node)}' has ${outgoing} outgoing paths (threshold ${MAX_PATHS}); consider decomposing the branching.`,\r\n messageKey: 'lint.minimizeRoutingPaths.outgoing',\r\n messageParams: { type: node.type, label: labelFor(node), count: outgoing, threshold: MAX_PATHS },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n if (incoming > MAX_PATHS) {\r\n out.push({\r\n ruleId: rule.id,\r\n severity: rule.defaultSeverity,\r\n message: `${friendlyType(node.type)} '${labelFor(node)}' has ${incoming} incoming paths (threshold ${MAX_PATHS}); consider merging upstream.`,\r\n messageKey: 'lint.minimizeRoutingPaths.incoming',\r\n messageParams: { type: node.type, label: labelFor(node), count: incoming, threshold: MAX_PATHS },\r\n elementId: node.id,\r\n processId: ctx.container.id,\r\n });\r\n }\r\n }\r\n return out;\r\n },\r\n};\r\n\r\nexport default rule;\r\n","/**\r\n * mindzie.BpmnEditor - lint rule catalogue\r\n *\r\n * Registers every shipped rule and exposes the canonical\r\n * `LINT_RULE_IDS` constant in a stable, deterministic order. The\r\n * order matters: it defines the order issues appear when multiple\r\n * rules fire on the same element (after element-id sort).\r\n *\r\n * Rule files live in `./rules/.ts`. Each file default-exports a\r\n * `LintRule` matching the contract in `./types.ts`.\r\n */\r\n\r\nimport type { LintRule } from './types.js';\r\n\r\nimport startEventRequired from './rules/start-event-required.js';\r\nimport endEventRequired from './rules/end-event-required.js';\r\nimport singleBlankStartEvent from './rules/single-blank-start-event.js';\r\nimport noDisconnected from './rules/no-disconnected.js';\r\nimport deadEnd from './rules/dead-end.js';\r\nimport unreachable from './rules/unreachable.js';\r\nimport danglingFlow from './rules/dangling-flow.js';\r\nimport duplicateId from './rules/duplicate-id.js';\r\nimport noImplicitStart from './rules/no-implicit-start.js';\r\nimport noImplicitEnd from './rules/no-implicit-end.js';\r\nimport noImplicitSplit from './rules/no-implicit-split.js';\r\nimport fakeJoin from './rules/fake-join.js';\r\nimport noGatewayJoinFork from './rules/no-gateway-join-fork.js';\r\nimport superfluousGateway from './rules/superfluous-gateway.js';\r\nimport superfluousTermination from './rules/superfluous-termination.js';\r\nimport noDuplicateSequenceFlows from './rules/no-duplicate-sequence-flows.js';\r\nimport noComplexGateway from './rules/no-complex-gateway.js';\r\nimport noInclusiveGateway from './rules/no-inclusive-gateway.js';\r\nimport eventBasedGateway from './rules/event-based-gateway.js';\r\nimport subProcessBlankStartEvent from './rules/sub-process-blank-start-event.js';\r\nimport eventSubProcessTypedStartEvent from './rules/event-sub-process-typed-start-event.js';\r\nimport conditionalFlows from './rules/conditional-flows.js';\r\nimport labelRequired from './rules/label-required.js';\r\nimport gatewayMismatch from './rules/gateway-mismatch.js';\r\nimport verbObjectActivityLabel from './rules/verb-object-activity-label.js';\r\nimport pastParticipleEventLabel from './rules/past-participle-event-label.js';\r\nimport decomposeLargeProcess from './rules/decompose-large-process.js';\r\nimport minimizeRoutingPaths from './rules/minimize-routing-paths.js';\r\n\r\n/**\r\n * Every rule shipped by Phase 12. Order groups by category for\r\n * readable rule-picker UIs:\r\n * 1. process structure (start/end/disconnect/dangling/duplicate)\r\n * 2. control-flow modeling (implicit / fake / gateway shape)\r\n * 3. event semantics (event-based, subprocess start types)\r\n * 4. flow conditions (conditional-flows)\r\n * 5. style / hygiene (label, complex/inclusive, superfluous)\r\n */\r\nexport const ALL_RULES: ReadonlyArray = [\r\n // 1. Process structure\r\n startEventRequired,\r\n endEventRequired,\r\n singleBlankStartEvent,\r\n noDisconnected,\r\n deadEnd,\r\n unreachable,\r\n danglingFlow,\r\n duplicateId,\r\n // 2. Control-flow modeling\r\n noImplicitStart,\r\n noImplicitEnd,\r\n noImplicitSplit,\r\n fakeJoin,\r\n noGatewayJoinFork,\r\n noDuplicateSequenceFlows,\r\n superfluousGateway,\r\n gatewayMismatch,\r\n // 3. Event semantics\r\n eventBasedGateway,\r\n subProcessBlankStartEvent,\r\n eventSubProcessTypedStartEvent,\r\n // 4. Flow conditions\r\n conditionalFlows,\r\n // 5. Style / hygiene\r\n labelRequired,\r\n noComplexGateway,\r\n noInclusiveGateway,\r\n superfluousTermination,\r\n // 6. Naming conventions (Phase 18 - 7PMG, info-level, opt-in)\r\n verbObjectActivityLabel,\r\n pastParticipleEventLabel,\r\n decomposeLargeProcess,\r\n minimizeRoutingPaths,\r\n];\r\n\r\n/** Stable rule-id array in catalogue order. Exported for rule pickers. */\r\nexport const LINT_RULE_IDS: ReadonlyArray = ALL_RULES.map((r) => r.id);\r\n\r\nexport type LintRuleId = (typeof LINT_RULE_IDS)[number];\r\n\r\n/**\r\n * Backward-compatibility map: legacy rule ids -> current ids. Callers\r\n * passing legacy names in `disabledRules` get translated transparently\r\n * for one release. Stable for tools that depend on old names.\r\n */\r\nexport const RENAMED_RULE_IDS: Readonly> = {\r\n 'no-start-event': 'start-event-required',\r\n 'no-end-event': 'end-event-required',\r\n 'orphan-flow-node': 'no-disconnected',\r\n 'missing-label': 'label-required',\r\n};\r\n","/**\r\n * mindzie.BpmnEditor - lint orchestrator\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Public entry point for the lint engine. Walks every container in\r\n * the document (top-level processes and nested subprocesses), builds\r\n * a precomputed `ContainerLintContext` once per container, runs each\r\n * enabled rule, and returns the findings sorted deterministically.\r\n */\r\n\r\nimport type { BpmnDocument } from '../base.js';\r\nimport type { LintIssue, LintOptions, LintQuickFix } from './types.js';\r\nimport { ALL_RULES, LINT_RULE_IDS, RENAMED_RULE_IDS } from './catalogue.js';\r\nimport { buildContainerContext, iterateContainers } from './helpers.js';\r\n\r\nexport type {\r\n LintIssue,\r\n LintOptions,\r\n LintSeverity,\r\n LintQuickFix,\r\n LintQuickFixEditor,\r\n} from './types.js';\r\nexport { LINT_RULE_IDS, RENAMED_RULE_IDS } from './catalogue.js';\r\nexport type { LintRuleId } from './catalogue.js';\r\n\r\n/**\r\n * Phase 14a: returns every quick-fix the matching rule offers for a\r\n * given issue. Tools render these as buttons / links in the lint UI.\r\n */\r\nexport function collectQuickFixes(\r\n document: BpmnDocument,\r\n issue: LintIssue,\r\n): LintQuickFix[] {\r\n const rule = ALL_RULES.find((r) => r.id === issue.ruleId);\r\n if (!rule || !rule.quickFixes) return [];\r\n try {\r\n return rule.quickFixes(issue, document);\r\n } catch (err) {\r\n // Same defensive policy as the check phase: a thrown error in one\r\n // rule shouldn't break the UI.\r\n // eslint-disable-next-line no-console\r\n console.error(`[lint] rule '${rule.id}'.quickFixes threw:`, err);\r\n return [];\r\n }\r\n}\r\n\r\n/**\r\n * Run every enabled rule over the document. Safe to call on any\r\n * parsed `BpmnDocument`; never throws.\r\n *\r\n * Renamed rule ids (e.g. legacy `no-start-event` is now\r\n * `start-event-required`) are translated in `disabledRules` for\r\n * backward compatibility.\r\n */\r\nexport function lintDocument(\r\n document: BpmnDocument,\r\n options: LintOptions = {},\r\n): LintIssue[] {\r\n const disabled = normaliseDisabled(options.disabledRules);\r\n const issues: LintIssue[] = [];\r\n\r\n // Container-scoped rules: one pass over every container, dispatch\r\n // each enabled rule with the precomputed context.\r\n for (const { container, isTopLevel } of iterateContainers(document)) {\r\n const ctx = buildContainerContext(container, isTopLevel, document);\r\n for (const rule of ALL_RULES) {\r\n if (disabled.has(rule.id)) continue;\r\n if (!rule.checkContainer) continue;\r\n try {\r\n issues.push(...rule.checkContainer(ctx));\r\n } catch (err) {\r\n // A rule throwing is a bug in the rule, not user input. Swallow\r\n // and continue so one broken rule doesn't kill the whole report.\r\n // eslint-disable-next-line no-console\r\n console.error(`[lint] rule '${rule.id}' threw:`, err);\r\n }\r\n }\r\n }\r\n\r\n // Document-scoped rules: run once on the full document.\r\n for (const rule of ALL_RULES) {\r\n if (disabled.has(rule.id)) continue;\r\n if (!rule.checkDocument) continue;\r\n try {\r\n issues.push(...rule.checkDocument(document));\r\n } catch (err) {\r\n // eslint-disable-next-line no-console\r\n console.error(`[lint] rule '${rule.id}' threw:`, err);\r\n }\r\n }\r\n\r\n return sortIssues(issues);\r\n}\r\n\r\nfunction normaliseDisabled(disabledRules: readonly string[] | undefined): Set {\r\n const out = new Set();\r\n if (!disabledRules) return out;\r\n for (const id of disabledRules) {\r\n out.add(id);\r\n const renamed = RENAMED_RULE_IDS[id];\r\n if (renamed !== undefined) {\r\n out.add(renamed);\r\n }\r\n }\r\n return out;\r\n}\r\n\r\nfunction sortIssues(issues: LintIssue[]): LintIssue[] {\r\n // Stable deterministic order: rule id, then element id, then message.\r\n return [...issues].sort((a, b) => {\r\n if (a.ruleId !== b.ruleId) return a.ruleId < b.ruleId ? -1 : 1;\r\n const ae = a.elementId ?? '';\r\n const be = b.elementId ?? '';\r\n if (ae !== be) return ae < be ? -1 : 1;\r\n return a.message < b.message ? -1 : a.message > b.message ? 1 : 0;\r\n });\r\n}\r\n\r\n// `LINT_RULE_IDS` is re-exported above as a `ReadonlyArray`; keep the\r\n// historic type-narrowed alias for callers that use it as a tuple.\r\nexport { LINT_RULE_IDS as default };\r\n","/**\r\n * mindzie.BpmnEditor - outline tree\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: S2 from IMPLEMENTATION_STATUS.md 3.2 - a hierarchical\r\n * tree view of the BPMN document suitable for click-to-select\r\n * navigation. Walks the natural BPMN containment:\r\n * - Collaboration -> Participant -> Process -> flowElements\r\n * - Process -> flowElements (task/event/gateway/flow/...)\r\n * - SubProcess (or adHocSubProcess/transaction) -> child flowElements\r\n * - Lane -> flowNodeRefs (reported as shallow children of the lane)\r\n *\r\n * Sequence flows are included; their source/target are recorded so\r\n * callers can render \"A -> B\" style entries. Artifacts (text annotation,\r\n * group) are included at the end of each container.\r\n *\r\n * Pure: no DOM, no view types. Callers render however they like.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { BpmnDocument, BpmnElement } from './base.js';\r\nimport type { Process } from './process.js';\r\nimport type { Collaboration, Participant, Lane, LaneSet } from './collaboration.js';\r\nimport type { SequenceFlow } from './flows.js';\r\n\r\nexport interface OutlineNode {\r\n id: string;\r\n type: string;\r\n /** Human-visible label - the element's name if present, else its id. */\r\n label: string;\r\n /**\r\n * Source->target summary for flow elements, e.g. \"Receive claim -> Review claim\".\r\n * Undefined for shapes.\r\n */\r\n flowSummary?: string;\r\n /** Child outline nodes. */\r\n children: OutlineNode[];\r\n}\r\n\r\n/**\r\n * Produces an outline tree rooted at the document's root elements.\r\n * Every visible BPMN element appears exactly once; ordering within\r\n * each container matches the BPMN source order (parser's bodyOrder).\r\n */\r\nexport function buildOutline(document: BpmnDocument): OutlineNode[] {\r\n const nodes: OutlineNode[] = [];\r\n for (const root of document.rootElements) {\r\n const node = buildNode(root, document);\r\n if (node !== null) {\r\n nodes.push(node);\r\n }\r\n }\r\n return nodes;\r\n}\r\n\r\nfunction buildNode(\r\n element: BpmnElement,\r\n document: BpmnDocument,\r\n): OutlineNode | null {\r\n if (isHiddenType(element.type)) return null;\r\n\r\n const node: OutlineNode = {\r\n id: element.id,\r\n type: element.type,\r\n label: labelFor(element),\r\n children: [],\r\n };\r\n\r\n if (element.type === 'sequenceFlow') {\r\n const flow = element as SequenceFlow;\r\n const from = resolveName(flow.sourceRef, document);\r\n const to = resolveName(flow.targetRef, document);\r\n if (from !== null && to !== null) {\r\n node.flowSummary = from + ' -> ' + to;\r\n }\r\n return node;\r\n }\r\n\r\n if (element.type === 'process') {\r\n return addProcessChildren(element as Process, node, document);\r\n }\r\n if (element.type === 'collaboration') {\r\n return addCollaborationChildren(element as Collaboration, node, document);\r\n }\r\n if (\r\n element.type === 'subProcess' ||\r\n element.type === 'adHocSubProcess' ||\r\n element.type === 'transaction'\r\n ) {\r\n return addFlowElementChildren(element, node, document);\r\n }\r\n if (element.type === 'participant') {\r\n const participant = element as Participant;\r\n if (participant.processRef !== undefined) {\r\n const process = document.byId.get(participant.processRef);\r\n if (process && process.type === 'process') {\r\n const pNode = buildNode(process, document);\r\n if (pNode !== null) node.children.push(pNode);\r\n }\r\n }\r\n return node;\r\n }\r\n if (element.type === 'lane') {\r\n const lane = element as Lane;\r\n for (const ref of lane.flowNodeRef) {\r\n const referenced = document.byId.get(ref);\r\n if (referenced !== undefined) {\r\n node.children.push({\r\n id: referenced.id,\r\n type: referenced.type,\r\n label: labelFor(referenced),\r\n children: [],\r\n });\r\n }\r\n }\r\n if (lane.childLaneSet !== undefined) {\r\n const laneSet = lane.childLaneSet as LaneSet;\r\n for (const subLane of laneSet.lanes ?? []) {\r\n const child = buildNode(subLane, document);\r\n if (child !== null) node.children.push(child);\r\n }\r\n }\r\n return node;\r\n }\r\n return node;\r\n}\r\n\r\nfunction addProcessChildren(\r\n process: Process,\r\n node: OutlineNode,\r\n document: BpmnDocument,\r\n): OutlineNode {\r\n // Prefer laneSet-based ordering if lanes exist.\r\n const laneSets = process.laneSets ?? [];\r\n if (laneSets.length > 0) {\r\n for (const ls of laneSets) {\r\n const laneSetTyped = ls as LaneSet;\r\n for (const lane of laneSetTyped.lanes ?? []) {\r\n const laneNode = buildNode(lane, document);\r\n if (laneNode !== null) node.children.push(laneNode);\r\n }\r\n }\r\n }\r\n for (const fe of process.flowElements ?? []) {\r\n const child = buildNode(fe, document);\r\n if (child !== null) node.children.push(child);\r\n }\r\n for (const a of process.artifacts ?? []) {\r\n const child = buildNode(a, document);\r\n if (child !== null) node.children.push(child);\r\n }\r\n return node;\r\n}\r\n\r\nfunction addCollaborationChildren(\r\n collab: Collaboration,\r\n node: OutlineNode,\r\n document: BpmnDocument,\r\n): OutlineNode {\r\n for (const p of collab.participants ?? []) {\r\n const child = buildNode(p, document);\r\n if (child !== null) node.children.push(child);\r\n }\r\n for (const mf of collab.messageFlows ?? []) {\r\n const child = buildNode(mf, document);\r\n if (child !== null) node.children.push(child);\r\n }\r\n return node;\r\n}\r\n\r\nfunction addFlowElementChildren(\r\n element: BpmnElement,\r\n node: OutlineNode,\r\n document: BpmnDocument,\r\n): OutlineNode {\r\n const sp = element as BpmnElement & {\r\n flowElements?: BpmnElement[];\r\n artifacts?: BpmnElement[];\r\n };\r\n for (const fe of sp.flowElements ?? []) {\r\n const child = buildNode(fe, document);\r\n if (child !== null) node.children.push(child);\r\n }\r\n for (const a of sp.artifacts ?? []) {\r\n const child = buildNode(a, document);\r\n if (child !== null) node.children.push(child);\r\n }\r\n return node;\r\n}\r\n\r\nfunction labelFor(element: BpmnElement): string {\r\n if (element.name !== undefined && element.name.trim().length > 0) {\r\n return element.name;\r\n }\r\n return element.id;\r\n}\r\n\r\nfunction resolveName(\r\n ref: string | undefined,\r\n document: BpmnDocument,\r\n): string | null {\r\n if (ref === undefined || ref.length === 0) return null;\r\n const el = document.byId.get(ref);\r\n if (el === undefined) return null;\r\n return labelFor(el);\r\n}\r\n\r\nconst HIDDEN_TYPES: ReadonlySet = new Set([\r\n // Declarative root elements with no useful outline representation.\r\n 'message',\r\n 'error',\r\n 'signal',\r\n 'escalation',\r\n 'category',\r\n 'itemDefinition',\r\n 'dataStore',\r\n]);\r\n\r\nfunction isHiddenType(type: string): boolean {\r\n return HIDDEN_TYPES.has(type);\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - element search\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: S3 from IMPLEMENTATION_STATUS.md 3.2 - case-insensitive\r\n * substring search across every element in the document. Searches the\r\n * element's name first, then its id (so a search for \"Flow_1\" still\r\n * finds unnamed elements). Returns hits in document order.\r\n *\r\n * Pure model analysis; no DOM or view types.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { BpmnDocument, BpmnElement } from './base.js';\r\n\r\nexport interface SearchHit {\r\n id: string;\r\n type: string;\r\n /** Display label - name if present, else id. */\r\n label: string;\r\n /** Which field matched: `'name'` or `'id'`. */\r\n matchedField: 'name' | 'id';\r\n}\r\n\r\nexport interface SearchOptions {\r\n /**\r\n * Maximum number of hits to return. Default 50. Pass 0 for unlimited.\r\n */\r\n limit?: number;\r\n}\r\n\r\n/**\r\n * Searches `document.byId` for elements whose name or id contains\r\n * `query` (case-insensitive). Returns an empty array when the query is\r\n * empty or whitespace-only.\r\n */\r\nexport function findElements(\r\n document: BpmnDocument,\r\n query: string,\r\n options: SearchOptions = {},\r\n): SearchHit[] {\r\n const trimmed = query.trim();\r\n if (trimmed.length === 0) return [];\r\n const limit = options.limit ?? 50;\r\n const needle = trimmed.toLowerCase();\r\n const hits: SearchHit[] = [];\r\n for (const [, element] of document.byId) {\r\n if (limit > 0 && hits.length >= limit) break;\r\n const hit = scoreElement(element, needle);\r\n if (hit !== null) hits.push(hit);\r\n }\r\n return hits;\r\n}\r\n\r\nfunction scoreElement(\r\n element: BpmnElement,\r\n needleLower: string,\r\n): SearchHit | null {\r\n const name = element.name;\r\n if (name !== undefined && name.toLowerCase().includes(needleLower)) {\r\n return {\r\n id: element.id,\r\n type: element.type,\r\n label: name,\r\n matchedField: 'name',\r\n };\r\n }\r\n if (element.id.toLowerCase().includes(needleLower)) {\r\n return {\r\n id: element.id,\r\n type: element.type,\r\n label: name !== undefined && name.length > 0 ? name : element.id,\r\n matchedField: 'id',\r\n };\r\n }\r\n return null;\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - BPMN document diff (Phase 21)\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Compute the structural difference between two parsed BPMN documents.\r\n * Returns a per-element categorisation: added (in B not A), removed\r\n * (in A not B), modified (in both with differing properties), or\r\n * unchanged.\r\n *\r\n * Matching strategy (in order, first match wins):\r\n * 1. Same id - exact id match\r\n * 2. Same type + same name + at least one neighbour-id in common\r\n * 3. Same type + same name + structurally similar position in flow\r\n * (incoming/outgoing flow count matches)\r\n *\r\n * Sequence flows match by (source id, target id) pair so a renamed\r\n * source/target produces a clean removed/added pair.\r\n */\r\n\r\nimport type { BpmnDocument, BpmnElement } from './index.js';\r\nimport type { SequenceFlow } from './flows.js';\r\n\r\n/** Diff status assigned to each compared element. */\r\nexport type DiffStatus = 'added' | 'removed' | 'modified' | 'unchanged';\r\n\r\n/** Per-element diff finding. */\r\nexport interface DiffEntry {\r\n /**\r\n * The element id used to identify this entry. For matched elements\r\n * this is the id from document B (the \"after\" / \"current\" doc);\r\n * for `removed` entries it's the id from document A.\r\n */\r\n elementId: string;\r\n /** Element type (e.g. 'task', 'sequenceFlow'). */\r\n elementType: string;\r\n /** Display label - prefers name, falls back to id. */\r\n label: string;\r\n /** What kind of change. */\r\n status: DiffStatus;\r\n /**\r\n * For `modified` entries: the property keys whose values differ\r\n * between A and B. Common entries: 'name', 'sourceRef', 'targetRef',\r\n * 'conditionExpression', 'gatewayDirection'.\r\n */\r\n changedFields?: string[];\r\n}\r\n\r\n/** Diff result over a whole document. */\r\nexport interface DiffResult {\r\n /** Every per-element finding. */\r\n entries: DiffEntry[];\r\n /** Count by status, for quick header rendering. */\r\n counts: Record;\r\n}\r\n\r\n/**\r\n * Compute the diff between two parsed BPMN documents.\r\n * `a` is the \"before\" / baseline, `b` is the \"after\" / current.\r\n */\r\nexport function diffDocuments(a: BpmnDocument, b: BpmnDocument): DiffResult {\r\n const elemsA = collectComparable(a);\r\n const elemsB = collectComparable(b);\r\n\r\n // Phase 1: pair by exact id match.\r\n const aById = new Map();\r\n for (const e of elemsA) aById.set(e.id, e);\r\n const bById = new Map();\r\n for (const e of elemsB) bById.set(e.id, e);\r\n\r\n const matched = new Map(); // bId -> aElement\r\n const unmatchedA: BpmnElement[] = [];\r\n const unmatchedB: BpmnElement[] = [];\r\n\r\n for (const ae of elemsA) {\r\n const be = bById.get(ae.id);\r\n if (be && be.type === ae.type) {\r\n matched.set(be.id, ae);\r\n } else {\r\n unmatchedA.push(ae);\r\n }\r\n }\r\n for (const be of elemsB) {\r\n if (!matched.has(be.id)) {\r\n unmatchedB.push(be);\r\n }\r\n }\r\n\r\n // Phase 2: pair unmatched by (type, name) - lets a rename-of-id pair\r\n // up cleanly without producing a phantom remove+add.\r\n const remainingA: BpmnElement[] = [];\r\n for (const ae of unmatchedA) {\r\n const idx = unmatchedB.findIndex(\r\n (be) => be.type === ae.type && (ae.name ?? '').length > 0 && be.name === ae.name,\r\n );\r\n if (idx >= 0) {\r\n const be = unmatchedB[idx]!;\r\n matched.set(be.id, ae);\r\n unmatchedB.splice(idx, 1);\r\n } else {\r\n remainingA.push(ae);\r\n }\r\n }\r\n\r\n // Build entries.\r\n const entries: DiffEntry[] = [];\r\n for (const [bId, ae] of matched) {\r\n const be = bById.get(bId)!;\r\n const changed = changedFields(ae, be);\r\n entries.push({\r\n elementId: bId,\r\n elementType: be.type,\r\n label: labelFor(be),\r\n status: changed.length === 0 ? 'unchanged' : 'modified',\r\n ...(changed.length > 0 ? { changedFields: changed } : {}),\r\n });\r\n }\r\n for (const ae of remainingA) {\r\n entries.push({\r\n elementId: ae.id,\r\n elementType: ae.type,\r\n label: labelFor(ae),\r\n status: 'removed',\r\n });\r\n }\r\n for (const be of unmatchedB) {\r\n entries.push({\r\n elementId: be.id,\r\n elementType: be.type,\r\n label: labelFor(be),\r\n status: 'added',\r\n });\r\n }\r\n\r\n // Counts.\r\n const counts: Record = {\r\n added: 0,\r\n removed: 0,\r\n modified: 0,\r\n unchanged: 0,\r\n };\r\n for (const e of entries) counts[e.status] += 1;\r\n\r\n // Sort: by status (added, removed, modified, unchanged), then by id.\r\n const statusOrder: Record = {\r\n added: 0,\r\n removed: 1,\r\n modified: 2,\r\n unchanged: 3,\r\n };\r\n entries.sort((x, y) => {\r\n if (x.status !== y.status) return statusOrder[x.status] - statusOrder[y.status];\r\n return x.elementId < y.elementId ? -1 : x.elementId > y.elementId ? 1 : 0;\r\n });\r\n\r\n return { entries, counts };\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Helpers */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Collect every flow node + sequence flow we should compare. Skips\r\n * DI elements, processes, collaborations etc. */\r\nfunction collectComparable(doc: BpmnDocument): BpmnElement[] {\r\n const out: BpmnElement[] = [];\r\n for (const root of doc.rootElements) {\r\n walkProcess(root, out);\r\n }\r\n return out;\r\n}\r\n\r\nconst COMPARABLE_TYPES: ReadonlySet = new Set([\r\n 'startEvent', 'endEvent', 'intermediateThrowEvent', 'intermediateCatchEvent',\r\n 'boundaryEvent', 'task', 'userTask', 'serviceTask', 'sendTask', 'receiveTask',\r\n 'manualTask', 'scriptTask', 'businessRuleTask', 'callActivity',\r\n 'subProcess', 'adHocSubProcess', 'transaction',\r\n 'exclusiveGateway', 'parallelGateway', 'inclusiveGateway',\r\n 'eventBasedGateway', 'complexGateway',\r\n 'sequenceFlow',\r\n]);\r\n\r\nfunction walkProcess(elem: BpmnElement, out: BpmnElement[]): void {\r\n if (COMPARABLE_TYPES.has(elem.type)) out.push(elem);\r\n const container = elem as BpmnElement & { flowElements?: BpmnElement[] };\r\n if (Array.isArray(container.flowElements)) {\r\n for (const child of container.flowElements) walkProcess(child, out);\r\n }\r\n // Recurse into collaboration -> participant -> processRef NOT followed\r\n // here; we walk it through doc.rootElements separately.\r\n}\r\n\r\n/** Fields that, when different, mark the element as 'modified'. */\r\nconst COMPARED_FIELDS: readonly string[] = [\r\n 'name',\r\n 'sourceRef',\r\n 'targetRef',\r\n 'conditionExpression',\r\n 'gatewayDirection',\r\n 'default',\r\n 'isExecutable',\r\n 'triggeredByEvent',\r\n];\r\n\r\nfunction changedFields(a: BpmnElement, b: BpmnElement): string[] {\r\n const diffs: string[] = [];\r\n const ao = a as unknown as Record;\r\n const bo = b as unknown as Record;\r\n for (const k of COMPARED_FIELDS) {\r\n const av = ao[k];\r\n const bv = bo[k];\r\n // Loose equality for undefined/empty-string asymmetry.\r\n if ((av ?? '') !== (bv ?? '')) {\r\n diffs.push(k);\r\n }\r\n }\r\n // For sequence flows, the source/target IDs may differ if the matched\r\n // flow had a renamed endpoint - changedFields catches that too.\r\n return diffs;\r\n}\r\n\r\nfunction labelFor(elem: BpmnElement): string {\r\n if (elem.name && elem.name.trim().length > 0) return elem.name;\r\n if (elem.type === 'sequenceFlow') {\r\n const f = elem as SequenceFlow;\r\n return `${f.sourceRef} → ${f.targetRef}`;\r\n }\r\n return elem.id;\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - obstacle collection for the router\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: translation from the scene's `LayoutResult` to the\r\n * rectangle list the orthogonal router consumes. Filters out the\r\n * routing endpoints themselves (since they are not obstacles for their\r\n * own flow) and applies a uniform padding so routes do not graze shape\r\n * edges. Pool / lane containers are skipped because they are layout\r\n * containers - routing inside them is expected.\r\n *\r\n * Pure: depends only on the `LayoutResult` shape and the `Rect` type.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { LayoutResult } from '../layout.js';\r\nimport type { Rect } from './geometry.js';\r\nimport { rectInflate } from './geometry.js';\r\n\r\nexport interface CollectObstacleOptions {\r\n /** Padding added around every obstacle. Default 8 scene units. */\r\n padding?: number;\r\n /** Element ids to exclude (typically the source + target of the flow). */\r\n exclude?: readonly string[];\r\n /**\r\n * Element types that are treated as routing containers, not\r\n * obstacles. Default: pool / lane variants.\r\n */\r\n containerTypes?: ReadonlySet;\r\n}\r\n\r\nconst DEFAULT_CONTAINER_TYPES: ReadonlySet = new Set([\r\n 'participant',\r\n 'lane',\r\n 'laneSet',\r\n]);\r\n\r\n/**\r\n * Returns a padded obstacle list drawn from `layout.shapes`, skipping\r\n * the excluded ids and the pool / lane containers.\r\n */\r\nexport function collectObstacles(\r\n layout: LayoutResult,\r\n typeOf: (id: string) => string | undefined,\r\n options: CollectObstacleOptions = {},\r\n): Rect[] {\r\n const padding = options.padding ?? 8;\r\n const exclude = new Set(options.exclude ?? []);\r\n const containerTypes = options.containerTypes ?? DEFAULT_CONTAINER_TYPES;\r\n const out: Rect[] = [];\r\n for (const [id, shape] of layout.shapes) {\r\n if (exclude.has(id)) continue;\r\n const t = typeOf(id);\r\n if (t !== undefined && containerTypes.has(t)) continue;\r\n out.push(rectInflate(shape.bounds, padding));\r\n }\r\n return out;\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - route-quality diagnostics\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: objective route-quality diagnostics for cleanup,\r\n * benchmark harnesses, and public QA reporting.\r\n *\r\n * Scores an existing BPMN DI polyline against a freshly routed\r\n * alternative. Used by cleanup commands, QA harnesses, and tests so\r\n * routing polish is measured by objective properties instead of visual\r\n * inspection alone.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { Point, Rect } from './geometry.js';\r\nimport { rectCenter, routeCost, sideFacesPoint } from './geometry.js';\r\nimport { whichSide } from './anchors.js';\r\nimport { pathIntersectsAny } from './route.js';\r\n\r\nexport type RouteQualityIssue =\r\n | 'shapeIntersection'\r\n | 'freshRouteBlocked'\r\n | 'excessiveBends'\r\n | 'excessiveCost'\r\n | 'wrongSideDock'\r\n | 'nonOrthogonalSegment'\r\n | 'redundantWaypoint'\r\n | 'overlappingSegment';\r\n\r\nexport interface RouteQualityInput {\r\n flowId: string;\r\n current: readonly Point[];\r\n fresh: readonly Point[];\r\n sourceBounds: Rect;\r\n targetBounds: Rect;\r\n obstacles: readonly Rect[];\r\n /** Other flow polylines used to detect same-lane overlaps. */\r\n otherRoutes?: readonly (readonly Point[])[];\r\n /** Cost multiplier above which current route is considered excessive. */\r\n costFactor?: number;\r\n /** Bend surplus above which current route is considered excessive. */\r\n bendSurplus?: number;\r\n}\r\n\r\nexport interface RouteQualityResult {\r\n flowId: string;\r\n currentCost: number;\r\n freshCost: number;\r\n currentBends: number;\r\n freshBends: number;\r\n currentBlocked: boolean;\r\n freshBlocked: boolean;\r\n wrongSideDock: boolean;\r\n nonOrthogonalSegments: number;\r\n redundantWaypoints: number;\r\n overlappingSegments: number;\r\n issues: RouteQualityIssue[];\r\n shouldAutoClean: boolean;\r\n}\r\n\r\nexport function evaluateRouteQuality(input: RouteQualityInput): RouteQualityResult {\r\n const costFactor = input.costFactor ?? 2;\r\n const bendSurplus = input.bendSurplus ?? 4;\r\n const currentCost = routeCost(input.current);\r\n const freshCost = routeCost(input.fresh);\r\n const currentBends = countBends(input.current);\r\n const freshBends = countBends(input.fresh);\r\n const currentBlocked = pathIntersectsAny(input.current, input.obstacles);\r\n const freshBlocked = pathIntersectsAny(input.fresh, input.obstacles);\r\n const wrongSideDock = hasWrongSideDock(\r\n input.current,\r\n input.sourceBounds,\r\n input.targetBounds,\r\n );\r\n const nonOrthogonalSegments = countNonOrthogonalSegments(input.current);\r\n const redundantWaypoints = countRedundantWaypoints(input.current);\r\n const overlappingSegments = countOverlappingSegments(\r\n input.current,\r\n input.otherRoutes ?? [],\r\n );\r\n\r\n const issues: RouteQualityIssue[] = [];\r\n if (currentBlocked) issues.push('shapeIntersection');\r\n if (freshBlocked) issues.push('freshRouteBlocked');\r\n if (currentBends > freshBends + bendSurplus) issues.push('excessiveBends');\r\n if (freshCost > 0 && currentCost > freshCost * costFactor) {\r\n issues.push('excessiveCost');\r\n }\r\n if (wrongSideDock) issues.push('wrongSideDock');\r\n if (nonOrthogonalSegments > 0) issues.push('nonOrthogonalSegment');\r\n if (redundantWaypoints > 0) issues.push('redundantWaypoint');\r\n if (overlappingSegments > 0) issues.push('overlappingSegment');\r\n\r\n const shouldAutoClean =\r\n !freshBlocked &&\r\n (currentBlocked ||\r\n nonOrthogonalSegments > 0 ||\r\n wrongSideDock ||\r\n currentBends > freshBends + bendSurplus ||\r\n (freshCost > 0 && currentCost > freshCost * costFactor));\r\n\r\n return {\r\n flowId: input.flowId,\r\n currentCost,\r\n freshCost,\r\n currentBends,\r\n freshBends,\r\n currentBlocked,\r\n freshBlocked,\r\n wrongSideDock,\r\n nonOrthogonalSegments,\r\n redundantWaypoints,\r\n overlappingSegments,\r\n issues,\r\n shouldAutoClean,\r\n };\r\n}\r\n\r\nexport function countBends(path: readonly Point[]): number {\r\n let bends = 0;\r\n for (let i = 2; i < path.length; i += 1) {\r\n const a = path[i - 2];\r\n const b = path[i - 1];\r\n const c = path[i];\r\n if (a === undefined || b === undefined || c === undefined) continue;\r\n const firstHorizontal = a.y === b.y;\r\n const secondHorizontal = b.y === c.y;\r\n const firstVertical = a.x === b.x;\r\n const secondVertical = b.x === c.x;\r\n if (\r\n (firstHorizontal && secondVertical) ||\r\n (firstVertical && secondHorizontal)\r\n ) {\r\n bends += 1;\r\n }\r\n }\r\n return bends;\r\n}\r\n\r\nfunction countNonOrthogonalSegments(path: readonly Point[]): number {\r\n let count = 0;\r\n for (let i = 1; i < path.length; i += 1) {\r\n const a = path[i - 1];\r\n const b = path[i];\r\n if (a === undefined || b === undefined) continue;\r\n if (a.x !== b.x && a.y !== b.y) count += 1;\r\n }\r\n return count;\r\n}\r\n\r\nfunction countRedundantWaypoints(path: readonly Point[]): number {\r\n let count = 0;\r\n for (let i = 1; i < path.length - 1; i += 1) {\r\n const a = path[i - 1];\r\n const b = path[i];\r\n const c = path[i + 1];\r\n if (a === undefined || b === undefined || c === undefined) continue;\r\n const sameVertical = a.x === b.x && b.x === c.x;\r\n const sameHorizontal = a.y === b.y && b.y === c.y;\r\n if (sameVertical || sameHorizontal) count += 1;\r\n }\r\n return count;\r\n}\r\n\r\nfunction hasWrongSideDock(\r\n path: readonly Point[],\r\n sourceBounds: Rect,\r\n targetBounds: Rect,\r\n): boolean {\r\n const first = path[0];\r\n const last = path[path.length - 1];\r\n if (first === undefined || last === undefined) return false;\r\n const sourceSide = whichSide(first, sourceBounds);\r\n const targetSide = whichSide(last, targetBounds);\r\n return (\r\n (sourceSide !== undefined &&\r\n !sideFacesPoint(sourceBounds, sourceSide, rectCenter(targetBounds))) ||\r\n (targetSide !== undefined &&\r\n !sideFacesPoint(targetBounds, targetSide, rectCenter(sourceBounds)))\r\n );\r\n}\r\n\r\nfunction countOverlappingSegments(\r\n current: readonly Point[],\r\n others: readonly (readonly Point[])[],\r\n): number {\r\n let count = 0;\r\n for (let i = 1; i < current.length; i += 1) {\r\n const a = current[i - 1];\r\n const b = current[i];\r\n if (a === undefined || b === undefined) continue;\r\n for (const other of others) {\r\n for (let j = 1; j < other.length; j += 1) {\r\n const c = other[j - 1];\r\n const d = other[j];\r\n if (c === undefined || d === undefined) continue;\r\n if (segmentsOverlap(a, b, c, d)) {\r\n count += 1;\r\n break;\r\n }\r\n }\r\n }\r\n }\r\n return count;\r\n}\r\n\r\nfunction segmentsOverlap(a: Point, b: Point, c: Point, d: Point): boolean {\r\n if (a.x === b.x && c.x === d.x && a.x === c.x) {\r\n return spanOverlap(a.y, b.y, c.y, d.y) > 0;\r\n }\r\n if (a.y === b.y && c.y === d.y && a.y === c.y) {\r\n return spanOverlap(a.x, b.x, c.x, d.x) > 0;\r\n }\r\n return false;\r\n}\r\n\r\nfunction spanOverlap(a1: number, a2: number, b1: number, b2: number): number {\r\n const minA = Math.min(a1, a2);\r\n const maxA = Math.max(a1, a2);\r\n const minB = Math.min(b1, b2);\r\n const maxB = Math.max(b1, b2);\r\n return Math.max(0, Math.min(maxA, maxB) - Math.max(minA, minB));\r\n}\r\n","/**\r\n * mindzie.BpmnEditor — scene graph types and serializer\r\n * Copyright (c) mindzie 2026 — proprietary, all rights reserved\r\n *\r\n * Implements: the pure-data SVG scene tree produced by the renderer\r\n * pipeline. A SceneNode is a plain object (tag + attrs + children) with no\r\n * DOM coupling, which lets renderers and layout logic be tested without\r\n * jsdom. Mounting to the DOM happens in a single pass in `pipeline.ts`;\r\n * serialisation to an SVG string happens here for text-mode consumers\r\n * (snapshot tests, server-side rendering in Blazor sample export).\r\n *\r\n * Per IMPLEMENTATION_SPEC.md Section 15.3.1 and research-notes/phase-3.md\r\n * §1, §2 (pipeline + layers).\r\n *\r\n * Design influences: research-notes/phase-3.md §1, §2, §15. Our scene\r\n * tree is pure data and our primitives are single-call constructors —\r\n * intentionally not the chained-mutation-helper style common elsewhere\r\n * in the SVG-graph ecosystem.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Layer enum */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * The fixed root-level layer ordering inside the viewport group. SVG\r\n * Z-order is DOM-positional, so later layers render on top of earlier\r\n * ones. Enum values double as stable CSS class suffixes (`mz-layer-...`).\r\n * Order matters and is guaranteed by `LAYERS` below.\r\n */\r\nexport enum SceneLayer {\r\n /** Background rect / subtle grid / watermark. */\r\n Background = 'background',\r\n /** Pool bands and lane tracks. Drawn under everything. */\r\n Pools = 'pools',\r\n /**\r\n * Backdrop bodies: shapes that other diagram content is drawn over, so\r\n * they must paint UNDER the edges that run across them - the same\r\n * relationship pools already have.\r\n *\r\n * Two kinds qualify, for the same paint reason but not the same\r\n * semantic one:\r\n * - Expanded container bodies (sub-process / ad-hoc sub-process /\r\n * transaction), whose body is drop space for the elements they\r\n * hold. Collapsed sub-processes are opaque boxes with nothing\r\n * inside and stay in `Shapes` (#8208).\r\n * - `callActivity`, which is NOT drop space and can never be a\r\n * parent, but is routinely resized to enclose the context it\r\n * calls; in `Shapes` its opaque body hid the connectors beneath\r\n * it (#8233).\r\n *\r\n * Membership here says nothing about containment - see\r\n * `BACKDROP_ONLY_TYPES` in `pipeline.ts`.\r\n */\r\n Containers = 'containers',\r\n /** Sequence/message/association polylines. Under shapes. */\r\n Edges = 'edges',\r\n /** Activities, events, gateways, data shapes. */\r\n Shapes = 'shapes',\r\n /** External labels (event labels below circle, etc.). */\r\n Labels = 'labels',\r\n /** Selection rings, hover overlays, debug boxes. */\r\n Overlays = 'overlays',\r\n}\r\n\r\n/** Canonical bottom-to-top layer ordering. */\r\nexport const LAYERS: readonly SceneLayer[] = [\r\n SceneLayer.Background,\r\n SceneLayer.Pools,\r\n SceneLayer.Containers,\r\n SceneLayer.Edges,\r\n SceneLayer.Shapes,\r\n SceneLayer.Labels,\r\n SceneLayer.Overlays,\r\n];\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* SceneNode */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Attribute values permitted on scene nodes. Numbers are stringified\r\n * with sensible precision; booleans become \"true\"/\"false\"; null/undefined\r\n * skip the attribute entirely. We explicitly do not accept `any` — every\r\n * callsite supplies a concrete primitive.\r\n */\r\nexport type SceneAttrValue = string | number | boolean | null | undefined;\r\n\r\n/** A live SVG element described as plain data. */\r\nexport interface SceneNode {\r\n /** SVG tag name (e.g., `g`, `rect`, `path`). */\r\n tag: string;\r\n /** Attribute bag. Keys become SVG attributes; values serialised per above. */\r\n attrs: Record;\r\n /** Ordered child nodes. */\r\n children: SceneNode[];\r\n /** Optional text body (text / tspan / title / style only). */\r\n text?: string;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Scene root */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * The top-level render output: the `` element plus its structured\r\n * layer groups. The pipeline builds exactly one `SceneRoot` per call to\r\n * `render()`; the root is then either mounted to a container or serialised\r\n * to a string.\r\n */\r\nexport interface SceneRoot {\r\n /** The root `` scene node. */\r\n svg: SceneNode;\r\n /** Layer groups keyed by enum, in canonical draw order. */\r\n layers: Record;\r\n /** The `` child where arrow markers and styles live. */\r\n defs: SceneNode;\r\n /** The viewport `` that carries pan/zoom transform. */\r\n viewport: SceneNode;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Builder helpers */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Constructs a bare SceneNode. Callers pass attrs and optional children;\r\n * both are defensively shallow-copied so the returned node is independent.\r\n */\r\nexport function node(\r\n tag: string,\r\n attrs: Record = {},\r\n children: SceneNode[] = [],\r\n text?: string,\r\n): SceneNode {\r\n const result: SceneNode = {\r\n tag,\r\n attrs: { ...attrs },\r\n children: children.slice(),\r\n };\r\n if (text !== undefined) {\r\n result.text = text;\r\n }\r\n return result;\r\n}\r\n\r\n/**\r\n * Adds a child to an existing scene node in-place and returns the parent\r\n * for chaining. Used by renderers that iteratively build a group.\r\n */\r\nexport function appendChild(parent: SceneNode, child: SceneNode): SceneNode {\r\n parent.children.push(child);\r\n return parent;\r\n}\r\n\r\n/**\r\n * Builds the root `` with the fixed layer skeleton documented in\r\n * research-notes/phase-3.md §2. Caller populates the layers then mounts\r\n * or serialises.\r\n */\r\nexport function createSceneRoot(options: {\r\n viewBox?: string;\r\n className?: string;\r\n theme?: string;\r\n}): SceneRoot {\r\n const defs = node('defs', {});\r\n const layerNodes: Partial> = {};\r\n const layerChildren: SceneNode[] = [];\r\n for (const layer of LAYERS) {\r\n const group = node('g', {\r\n class: `mz-layer mz-layer-${layer}`,\r\n 'data-layer': layer,\r\n });\r\n layerNodes[layer] = group;\r\n layerChildren.push(group);\r\n }\r\n const viewport = node(\r\n 'g',\r\n {\r\n class: 'mz-viewport',\r\n transform: 'translate(0 0) scale(1)',\r\n },\r\n layerChildren,\r\n );\r\n const svgAttrs: Record = {\r\n xmlns: 'http://www.w3.org/2000/svg',\r\n class: options.className ?? 'mz-bpmn',\r\n width: '100%',\r\n height: '100%',\r\n 'data-theme': options.theme ?? 'mindzie-light',\r\n };\r\n if (options.viewBox !== undefined) {\r\n svgAttrs['viewBox'] = options.viewBox;\r\n svgAttrs['preserveAspectRatio'] = 'xMidYMid meet';\r\n }\r\n const svg = node('svg', svgAttrs, [defs, viewport]);\r\n return {\r\n svg,\r\n defs,\r\n viewport,\r\n // Cast safe: every enum value was assigned in the loop above.\r\n layers: layerNodes as Record,\r\n };\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Serialization */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Characters that must be escaped when emitting attribute values. */\r\nconst ATTR_ESCAPE: Record = {\r\n '&': '&',\r\n '<': '<',\r\n '>': '>',\r\n '\"': '"',\r\n \"'\": ''',\r\n};\r\n\r\n/** Characters that must be escaped in element text content. */\r\nconst TEXT_ESCAPE: Record = {\r\n '&': '&',\r\n '<': '<',\r\n '>': '>',\r\n};\r\n\r\nfunction escapeAttr(value: string): string {\r\n return value.replace(/[&<>\"']/g, (c) => ATTR_ESCAPE[c] ?? c);\r\n}\r\n\r\nfunction escapeText(value: string): string {\r\n return value.replace(/[&<>]/g, (c) => TEXT_ESCAPE[c] ?? c);\r\n}\r\n\r\n/**\r\n * Normalises an attribute value to a string, applying sensible precision\r\n * for numbers. Returns `null` to signal \"drop this attribute entirely\"\r\n * (for `null` / `undefined` inputs, or for `false` boolean values where\r\n * emitting the attribute would be misleading).\r\n */\r\nfunction formatAttrValue(value: SceneAttrValue): string | null {\r\n if (value === null || value === undefined) {\r\n return null;\r\n }\r\n if (typeof value === 'boolean') {\r\n return value ? 'true' : 'false';\r\n }\r\n if (typeof value === 'number') {\r\n // Three decimal places is plenty for SVG geometry and keeps output stable.\r\n if (Number.isInteger(value)) {\r\n return String(value);\r\n }\r\n return Number(value.toFixed(3)).toString();\r\n }\r\n return value;\r\n}\r\n\r\n/** Tags that are always emitted as self-closing when they have no children or text. */\r\nconst VOID_TAGS: ReadonlySet = new Set([\r\n 'circle',\r\n 'ellipse',\r\n 'line',\r\n 'path',\r\n 'polygon',\r\n 'polyline',\r\n 'rect',\r\n 'stop',\r\n 'use',\r\n]);\r\n\r\n/**\r\n * Serialises a SceneNode tree to an SVG string. The output is suitable\r\n * for insertion into the DOM via `innerHTML`, for snapshot tests, and\r\n * for server-side rendering. No XML declaration is emitted; the caller\r\n * chooses wrapping.\r\n */\r\nexport function serializeScene(node: SceneNode): string {\r\n const out: string[] = [];\r\n writeNode(node, out);\r\n return out.join('');\r\n}\r\n\r\nfunction writeNode(n: SceneNode, out: string[]): void {\r\n out.push('<', n.tag);\r\n const keys = Object.keys(n.attrs);\r\n for (const key of keys) {\r\n const raw = n.attrs[key];\r\n const formatted = formatAttrValue(raw);\r\n if (formatted === null) {\r\n continue;\r\n }\r\n out.push(' ', key, '=\"', escapeAttr(formatted), '\"');\r\n }\r\n const hasText = n.text !== undefined && n.text.length > 0;\r\n const hasChildren = n.children.length > 0;\r\n if (!hasText && !hasChildren && VOID_TAGS.has(n.tag)) {\r\n out.push(' />');\r\n return;\r\n }\r\n out.push('>');\r\n if (hasText && n.text !== undefined) {\r\n out.push(escapeText(n.text));\r\n }\r\n for (const child of n.children) {\r\n writeNode(child, out);\r\n }\r\n out.push('');\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* DOM mount */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** SVG 1.1 namespace URI. */\r\nexport const SVG_NS = 'http://www.w3.org/2000/svg';\r\n\r\n/**\r\n * Mounts a scene node tree into real DOM. The returned Element is an\r\n * `SVGElement` for the root call and for children of SVG parents. The\r\n * function is recursive but iterative per-child so large diagrams mount\r\n * in a single walk (research-notes/phase-3.md §14).\r\n */\r\nexport function mountScene(n: SceneNode, doc: Document): SVGElement {\r\n const el = doc.createElementNS(SVG_NS, n.tag) as SVGElement;\r\n applyAttrs(el, n.attrs);\r\n if (n.text !== undefined && n.text.length > 0) {\r\n el.appendChild(doc.createTextNode(n.text));\r\n }\r\n for (const child of n.children) {\r\n el.appendChild(mountScene(child, doc));\r\n }\r\n return el;\r\n}\r\n\r\nfunction applyAttrs(el: SVGElement, attrs: Record): void {\r\n for (const key of Object.keys(attrs)) {\r\n const raw = attrs[key];\r\n const formatted = formatAttrValue(raw);\r\n if (formatted === null) {\r\n continue;\r\n }\r\n el.setAttribute(key, formatted);\r\n }\r\n}\r\n","/**\r\n * mindzie.BpmnEditor — SVG primitive constructors\r\n * Copyright (c) mindzie 2026 — proprietary, all rights reserved\r\n *\r\n * Implements: pure single-call constructors that return SceneNodes for\r\n * the common SVG building blocks the renderers need (rect, circle,\r\n * polygon, polyline, path, line, text, group). Per research-notes\r\n * /phase-3.md §4: our primitives take options objects and produce\r\n * fully-formed nodes in one call, intentionally avoiding the chained\r\n * mutator-combinator style common in other SVG-graph libraries.\r\n *\r\n * Per IMPLEMENTATION_SPEC.md Section 15.3.1.\r\n *\r\n * Design influences: research-notes/phase-3.md §4. SVG attribute names\r\n * sourced from MDN's SVG attribute reference (allowlisted) and the\r\n * W3C SVG 1.1 specification.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport { node, type SceneAttrValue, type SceneNode } from './scene.js';\r\n\r\n/** A 2D point. */\r\nexport interface Point {\r\n x: number;\r\n y: number;\r\n}\r\n\r\n/** Common stroke / fill options accepted by every shape primitive. */\r\nexport interface ShapeStyle {\r\n fill?: string;\r\n stroke?: string;\r\n /** Stroke width — number for explicit px or string for CSS `var()` references. */\r\n strokeWidth?: number | string;\r\n strokeDasharray?: string;\r\n strokeLinecap?: 'butt' | 'round' | 'square';\r\n strokeLinejoin?: 'miter' | 'round' | 'bevel';\r\n opacity?: number;\r\n className?: string;\r\n}\r\n\r\n/** Options for a rectangle. */\r\nexport interface RectOptions extends ShapeStyle {\r\n x: number;\r\n y: number;\r\n width: number;\r\n height: number;\r\n rx?: number;\r\n ry?: number;\r\n}\r\n\r\n/** Options for a circle. */\r\nexport interface CircleOptions extends ShapeStyle {\r\n cx: number;\r\n cy: number;\r\n r: number;\r\n}\r\n\r\n/** Options for a polygon (closed) or polyline (open). */\r\nexport interface PolyOptions extends ShapeStyle {\r\n points: readonly Point[];\r\n}\r\n\r\n/** Options for an arbitrary SVG path. */\r\nexport interface PathOptions extends ShapeStyle {\r\n /** SVG path data string. */\r\n d: string;\r\n /** Optional reference to a `` marker for the end of the path. */\r\n markerEnd?: string;\r\n /** Optional reference to a `` marker for the start of the path. */\r\n markerStart?: string;\r\n /** Optional reference to a `` marker for mid-points. */\r\n markerMid?: string;\r\n}\r\n\r\n/** Options for a straight line. */\r\nexport interface LineOptions extends ShapeStyle {\r\n x1: number;\r\n y1: number;\r\n x2: number;\r\n y2: number;\r\n}\r\n\r\n/** Options for a `` element. */\r\nexport interface TextOptions {\r\n x: number;\r\n y: number;\r\n text: string;\r\n fontFamily?: string;\r\n fontSize?: number;\r\n fontWeight?: string | number;\r\n textAnchor?: 'start' | 'middle' | 'end';\r\n dominantBaseline?: 'auto' | 'middle' | 'central' | 'hanging' | 'baseline';\r\n fill?: string;\r\n className?: string;\r\n /** Optional pre-wrapped lines rendered as `` children. */\r\n tspans?: readonly TspanOptions[];\r\n}\r\n\r\n/** A single line of wrapped text inside a ``. */\r\nexport interface TspanOptions {\r\n text: string;\r\n /** Absolute X — usually matches the parent text's X. */\r\n x?: number;\r\n /** Vertical advance for subsequent lines (e.g., `'1.2em'`). */\r\n dy?: string | number;\r\n}\r\n\r\n/** Options for a `` group. */\r\nexport interface GroupOptions {\r\n className?: string;\r\n transform?: string;\r\n /** Optional element-id stamp for hit-testing/debug. */\r\n dataElementId?: string;\r\n /** Arbitrary `data-*` attributes. Keys are emitted as written. */\r\n data?: Record;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Helpers */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Joins a list of points into the `points` attribute string. */\r\nexport function pointsToString(points: readonly Point[]): string {\r\n const parts: string[] = [];\r\n for (const p of points) {\r\n parts.push(`${formatNumber(p.x)},${formatNumber(p.y)}`);\r\n }\r\n return parts.join(' ');\r\n}\r\n\r\nfunction formatNumber(n: number): string {\r\n if (Number.isInteger(n)) {\r\n return String(n);\r\n }\r\n return Number(n.toFixed(3)).toString();\r\n}\r\n\r\n/**\r\n * Drops `undefined` entries from an attribute object so the SceneNode\r\n * does not carry stub attributes. Keeps the call sites tidy.\r\n */\r\nfunction attrs(input: Record): Record {\r\n const out: Record = {};\r\n for (const key of Object.keys(input)) {\r\n const value = input[key];\r\n if (value !== undefined && value !== null) {\r\n out[key] = value;\r\n }\r\n }\r\n return out;\r\n}\r\n\r\nfunction style(s: ShapeStyle): Record {\r\n return attrs({\r\n fill: s.fill,\r\n stroke: s.stroke,\r\n 'stroke-width': s.strokeWidth,\r\n 'stroke-dasharray': s.strokeDasharray,\r\n 'stroke-linecap': s.strokeLinecap,\r\n 'stroke-linejoin': s.strokeLinejoin,\r\n opacity: s.opacity,\r\n class: s.className,\r\n });\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Constructors */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Rounded or sharp rectangle. */\r\nexport function makeRect(opts: RectOptions): SceneNode {\r\n return node('rect', {\r\n x: opts.x,\r\n y: opts.y,\r\n width: opts.width,\r\n height: opts.height,\r\n rx: opts.rx,\r\n ry: opts.ry,\r\n ...style(opts),\r\n });\r\n}\r\n\r\n/** Circle. */\r\nexport function makeCircle(opts: CircleOptions): SceneNode {\r\n return node('circle', {\r\n cx: opts.cx,\r\n cy: opts.cy,\r\n r: opts.r,\r\n ...style(opts),\r\n });\r\n}\r\n\r\n/** Closed polygon. */\r\nexport function makePolygon(opts: PolyOptions): SceneNode {\r\n return node('polygon', {\r\n points: pointsToString(opts.points),\r\n ...style(opts),\r\n });\r\n}\r\n\r\n/** Open polyline. */\r\nexport function makePolyline(opts: PolyOptions): SceneNode {\r\n return node('polyline', {\r\n points: pointsToString(opts.points),\r\n ...style(opts),\r\n });\r\n}\r\n\r\n/** Arbitrary path. */\r\nexport function makePath(opts: PathOptions): SceneNode {\r\n return node('path', {\r\n d: opts.d,\r\n 'marker-start': opts.markerStart,\r\n 'marker-mid': opts.markerMid,\r\n 'marker-end': opts.markerEnd,\r\n ...style(opts),\r\n });\r\n}\r\n\r\n/** Straight line. */\r\nexport function makeLine(opts: LineOptions): SceneNode {\r\n return node('line', {\r\n x1: opts.x1,\r\n y1: opts.y1,\r\n x2: opts.x2,\r\n y2: opts.y2,\r\n ...style(opts),\r\n });\r\n}\r\n\r\n/** A `` with optional wrapped `` children. */\r\nexport function makeText(opts: TextOptions): SceneNode {\r\n const root = node('text', {\r\n x: opts.x,\r\n y: opts.y,\r\n 'font-family': opts.fontFamily,\r\n 'font-size': opts.fontSize,\r\n 'font-weight': opts.fontWeight,\r\n 'text-anchor': opts.textAnchor,\r\n 'dominant-baseline': opts.dominantBaseline,\r\n fill: opts.fill,\r\n class: opts.className,\r\n });\r\n if (opts.tspans && opts.tspans.length > 0) {\r\n for (const t of opts.tspans) {\r\n root.children.push(\r\n node(\r\n 'tspan',\r\n attrs({\r\n x: t.x,\r\n dy: t.dy,\r\n }),\r\n [],\r\n t.text,\r\n ),\r\n );\r\n }\r\n } else {\r\n root.text = opts.text;\r\n }\r\n return root;\r\n}\r\n\r\n/** A `` group. */\r\nexport function makeGroup(opts: GroupOptions = {}, children: SceneNode[] = []): SceneNode {\r\n const a: Record = {\r\n class: opts.className,\r\n transform: opts.transform,\r\n };\r\n if (opts.dataElementId !== undefined) {\r\n a['data-element-id'] = opts.dataElementId;\r\n }\r\n if (opts.data) {\r\n for (const key of Object.keys(opts.data)) {\r\n a[`data-${key}`] = opts.data[key] ?? '';\r\n }\r\n }\r\n return node('g', attrs(a), children.slice());\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Path data builders (small DSL used by edge / icon code) */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Builds an SVG path `d` string from a polyline. */\r\nexport function polylinePath(points: readonly Point[]): string {\r\n if (points.length === 0) {\r\n return '';\r\n }\r\n const head = points[0];\r\n if (head === undefined) {\r\n return '';\r\n }\r\n const parts: string[] = [`M ${formatNumber(head.x)} ${formatNumber(head.y)}`];\r\n for (let i = 1; i < points.length; i += 1) {\r\n const p = points[i];\r\n if (p === undefined) {\r\n continue;\r\n }\r\n parts.push(`L ${formatNumber(p.x)} ${formatNumber(p.y)}`);\r\n }\r\n return parts.join(' ');\r\n}\r\n","/**\r\n * mindzie.BpmnEditor — label placement and word wrap\r\n * Copyright (c) mindzie 2026 — proprietary, all rights reserved\r\n *\r\n * Implements: label placement helpers used by every family renderer.\r\n * `wrapText` honours author-encoded line breaks and then greedy-wraps\r\n * each paragraph to the supplied pixel width, ellipsising when the\r\n * `maxLines` budget is exhausted. `layoutLabel` chooses anchor /\r\n * baseline given a placement request. All pure functions; no DOM\r\n * measurement here (the measurer lives in `pipeline.ts`).\r\n *\r\n * Per IMPLEMENTATION_SPEC.md Section 15.3.2 and research-notes\r\n * /phase-3.md §9.\r\n *\r\n * Design influences: research-notes/phase-3.md §9 (greedy split,\r\n * 4-line cap, ellipsis). Character-width heuristic is the classic\r\n * \"~60% of font-size per ASCII character\" rule plus a 120% boost for\r\n * CJK / wide glyphs.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport { makeText, type TspanOptions } from '../primitives.js';\r\nimport type { SceneNode } from '../scene.js';\r\nimport type { TextMeasurer } from '../text-metrics.js';\r\n\r\n/** Where a label goes relative to its shape. */\r\nexport type LabelAnchor = 'inside' | 'below' | 'above' | 'left' | 'right' | 'center';\r\n\r\n/** Inputs for a simple shape label. */\r\nexport interface LabelOptions {\r\n /** Text to render (caller handles `undefined` name). */\r\n text: string;\r\n /** Anchor region the shape wants to use. */\r\n anchor: LabelAnchor;\r\n /** Bounding box of the parent shape. */\r\n shape: { x: number; y: number; width: number; height: number };\r\n /**\r\n * Optional DI-authored label bounds. When provided, the renderer uses\r\n * these bounds directly — ignoring `anchor` — so author-laid-out labels\r\n * (events, gateways, data objects) don't get clipped by our anchor-\r\n * based heuristic. Per BPMN-DI, `bpmndi:BPMNLabel/dc:Bounds` exists\r\n * precisely for this case.\r\n */\r\n labelBounds?: { x: number; y: number; width: number; height: number };\r\n /** Font size in px (defaults to 12). */\r\n fontSize?: number;\r\n /** Approx character width as a multiple of font size (defaults to 0.6). */\r\n charWidthFactor?: number;\r\n /** Inner padding on left and right when wrapping inside a shape. */\r\n innerPadding?: number;\r\n /**\r\n * Horizontal alignment for `inside` / `center` anchored text.\r\n *\r\n * Defaults to `'middle'`, which is right for a name centred in a task\r\n * box. Prose - a text annotation carrying a sentence of narrative -\r\n * reads badly centred, because every wrapped line starts in a\r\n * different place; pass `'start'` for those (#8238).\r\n */\r\n align?: 'start' | 'middle';\r\n /** Maximum visible lines (default 4). */\r\n maxLines?: number;\r\n /** Optional CSS class for the `` node. */\r\n className?: string;\r\n /** Optional explicit fill. */\r\n fill?: string;\r\n /** Optional explicit font family. */\r\n fontFamily?: string;\r\n /**\r\n * Real text measurer (#8224). When supplied, wrapping decides what\r\n * fits by MEASURING each candidate line instead of counting\r\n * characters, so the painted line count matches the box the label\r\n * planner reserved. Absent — headless environments that cannot\r\n * measure text — keeps the character-width heuristic below.\r\n */\r\n measureText?: TextMeasurer;\r\n}\r\n\r\n/** Output of word wrap: the line list and whether truncation happened. */\r\nexport interface WrapResult {\r\n lines: string[];\r\n truncated: boolean;\r\n}\r\n\r\n/**\r\n * Word-wraps `text` to fit in `widthPx` pixels, producing up to\r\n * `maxLines` line strings.\r\n *\r\n * Author-encoded line breaks (\\n, \\r, \\r\\n) are honoured verbatim.\r\n * Many BPMN authoring tools (ARIS, Bizagi, Trisotech) hand-lay-out\r\n * multi-line task labels by embedding explicit newlines AND sizing the\r\n * shape to fit that layout. Flattening them would destroy the intent.\r\n *\r\n * Within each paragraph we greedy-wrap by whitespace; tokens longer\r\n * than the line budget get hard-wrapped character-by-character so\r\n * runaway identifiers still fit. The last visible line is ellipsised\r\n * when any source content was dropped.\r\n *\r\n * `charWidth` is a rough horizontal metric in pixels per character.\r\n * Callers that have a real text measurer can supply the number; the\r\n * default is 0.6 * fontSize (well-tested rule of thumb for sans-serif\r\n * Latin at 12-16px).\r\n */\r\nexport function wrapText(\r\n text: string,\r\n widthPx: number,\r\n charWidth: number,\r\n maxLines: number = 4,\r\n): WrapResult {\r\n if (text.length === 0) {\r\n return { lines: [], truncated: false };\r\n }\r\n const effectiveCharWidth = charWidth > 0 ? charWidth : 7.2;\r\n const maxChars = Math.max(1, Math.floor(widthPx / effectiveCharWidth));\r\n return wrapWithMetrics(text, maxLines, {\r\n fits: (candidate) => candidate.length <= maxChars,\r\n split: (token) => {\r\n const out: string[] = [];\r\n for (let i = 0; i < token.length; i += maxChars) {\r\n out.push(token.slice(i, i + maxChars));\r\n }\r\n return out;\r\n },\r\n ellipsise: (line) => `${line.slice(0, Math.max(0, maxChars - 1))}…`,\r\n });\r\n}\r\n\r\n/**\r\n * Word-wraps `text` the same way {@link wrapText} does, but decides\r\n * what fits by MEASURING each candidate line rather than counting\r\n * characters (#8224).\r\n *\r\n * This is what lets the label planner and the label renderer agree: a\r\n * planned collision box is only honest if the text that gets painted\r\n * into it breaks onto the same number of lines the planner assumed, and\r\n * a character count cannot predict that for proportional fonts (\"WWW\"\r\n * and \"iii\" are the same three characters and nowhere near the same\r\n * width).\r\n *\r\n * `measure` returns the rendered width of one line; supply the same\r\n * measurer used to size the box.\r\n */\r\nexport function wrapTextMeasured(\r\n text: string,\r\n widthPx: number,\r\n measure: (line: string) => number,\r\n maxLines: number = 4,\r\n): WrapResult {\r\n if (text.length === 0) {\r\n return { lines: [], truncated: false };\r\n }\r\n const fits = (candidate: string): boolean => measure(candidate) <= widthPx;\r\n return wrapWithMetrics(text, maxLines, {\r\n fits,\r\n split: (token) => {\r\n const out: string[] = [];\r\n let chunk = '';\r\n for (const ch of token) {\r\n // A chunk always keeps at least one character, otherwise a\r\n // width too narrow for a single glyph would never terminate.\r\n if (chunk.length > 0 && !fits(chunk + ch)) {\r\n out.push(chunk);\r\n chunk = ch;\r\n continue;\r\n }\r\n chunk += ch;\r\n }\r\n if (chunk.length > 0) out.push(chunk);\r\n return out;\r\n },\r\n ellipsise: (line) => {\r\n let trimmed = line;\r\n while (trimmed.length > 0 && !fits(`${trimmed}…`)) {\r\n trimmed = trimmed.slice(0, -1);\r\n }\r\n return `${trimmed}…`;\r\n },\r\n });\r\n}\r\n\r\n/** How a wrap decides what fits on a line. */\r\ninterface WrapMetrics {\r\n /** True when `candidate` fits on a single line. */\r\n fits(candidate: string): boolean;\r\n /** Hard-splits a token too long for one line into line-sized chunks. */\r\n split(token: string): string[];\r\n /** Shortens `line` so that it plus a trailing ellipsis fits. */\r\n ellipsise(line: string): string;\r\n}\r\n\r\n/**\r\n * The shared greedy wrap. Both {@link wrapText} (character budget) and\r\n * {@link wrapTextMeasured} (measured budget) run this identical\r\n * algorithm; only the \"does it fit\" predicate differs.\r\n */\r\nfunction wrapWithMetrics(text: string, maxLines: number, metrics: WrapMetrics): WrapResult {\r\n const paragraphs = text.split(/\\r\\n|\\n|\\r/);\r\n const lines: string[] = [];\r\n let truncated = false;\r\n\r\n outer: for (let pIndex = 0; pIndex < paragraphs.length; pIndex += 1) {\r\n const paragraph = paragraphs[pIndex] ?? '';\r\n const normalised = paragraph.replace(/[\\t ]+/g, ' ').trim();\r\n if (normalised.length === 0) {\r\n if (\r\n lines.length > 0 &&\r\n lines.length < maxLines &&\r\n pIndex < paragraphs.length - 1\r\n ) {\r\n lines.push('');\r\n }\r\n continue;\r\n }\r\n const tokens = tokenise(normalised, metrics);\r\n let current = '';\r\n for (const tok of tokens) {\r\n if (current.length === 0) {\r\n current = tok;\r\n continue;\r\n }\r\n const joined = `${current} ${tok}`;\r\n if (metrics.fits(joined)) {\r\n current = joined;\r\n continue;\r\n }\r\n lines.push(current);\r\n if (lines.length === maxLines) {\r\n truncated = true;\r\n current = '';\r\n break outer;\r\n }\r\n current = tok;\r\n }\r\n if (current.length > 0) {\r\n if (lines.length < maxLines) {\r\n lines.push(current);\r\n } else {\r\n truncated = true;\r\n }\r\n }\r\n if (lines.length === maxLines && pIndex < paragraphs.length - 1) {\r\n truncated = true;\r\n break;\r\n }\r\n }\r\n\r\n if (!truncated) {\r\n // Sanity check: compare visible character counts so trailing\r\n // whitespace-only paragraphs don't spuriously flag truncation.\r\n const visibleSource = text.replace(/\\s+/g, '').length;\r\n const visibleEmitted = lines.join('').replace(/\\s+/g, '').length;\r\n if (visibleEmitted < visibleSource) {\r\n truncated = true;\r\n }\r\n }\r\n\r\n if (truncated && lines.length > 0) {\r\n const last = lines[lines.length - 1] ?? '';\r\n if (last.length > 1) {\r\n lines[lines.length - 1] = metrics.ellipsise(last);\r\n }\r\n }\r\n return { lines, truncated };\r\n}\r\n\r\nfunction tokenise(text: string, metrics: WrapMetrics): string[] {\r\n // Callers hand us a single paragraph with whitespace already collapsed.\r\n const parts = text.length === 0 ? [] : text.split(' ');\r\n const out: string[] = [];\r\n for (const part of parts) {\r\n if (part.length === 0) {\r\n continue;\r\n }\r\n if (metrics.fits(part)) {\r\n out.push(part);\r\n continue;\r\n }\r\n // Runaway token — hard-wrap to the line budget.\r\n for (const chunk of metrics.split(part)) {\r\n out.push(chunk);\r\n }\r\n }\r\n return out;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Connector label metrics (#8224) */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Font size connector labels are drawn at. */\r\nexport const EDGE_LABEL_FONT_SIZE = 11;\r\n\r\n/** Line budget for a connector label before it is ellipsised. */\r\nexport const EDGE_LABEL_MAX_LINES = 3;\r\n\r\n/** Baseline-to-baseline spacing, as a multiple of the font size. */\r\nexport const LABEL_LINE_HEIGHT_EM = 1.2;\r\n\r\n/**\r\n * Width a connector label wraps at. Wide enough that ordinary flow\r\n * names (\"Approved\", \"Rejected — send back to requester\") stay on one\r\n * or two lines, narrow enough that a long name becomes a compact block\r\n * rather than a ribbon lying across half the diagram.\r\n */\r\nexport const EDGE_LABEL_WRAP_WIDTH = 120;\r\n\r\n/** Horizontal breathing room between the text and the box edge. */\r\nconst EDGE_LABEL_PADDING_X = 4;\r\n\r\n/** Vertical breathing room between the text and the box edge. */\r\nconst EDGE_LABEL_PADDING_Y = 2;\r\n\r\n/** Floor so a one-glyph label still has a box worth colliding against. */\r\nconst EDGE_LABEL_MIN_WIDTH = 24;\r\n\r\n/** The measured footprint of a connector label. */\r\nexport interface EdgeLabelBox {\r\n width: number;\r\n height: number;\r\n /** The wrapped lines the box was sized for. */\r\n lines: string[];\r\n}\r\n\r\n/**\r\n * Measures the box a connector label will actually occupy (#8224).\r\n *\r\n * The label planner and the label renderer must agree on this number or\r\n * collision planning is fiction: the planner reserved a 110x14 guess\r\n * while the renderer painted up to three wrapped lines nearly three\r\n * times as tall. Both now call this one function with the same\r\n * measurer, so the box that was scored against lines, shapes and other\r\n * labels is the box that gets painted.\r\n *\r\n * The returned width includes padding, so re-wrapping the same text to\r\n * `width` can never produce MORE lines than `lines`.\r\n */\r\nexport function measureEdgeLabelBox(\r\n text: string,\r\n measure: TextMeasurer,\r\n fontSize: number = EDGE_LABEL_FONT_SIZE,\r\n maxLines: number = EDGE_LABEL_MAX_LINES,\r\n): EdgeLabelBox {\r\n const measureLine = (line: string): number => measure(line, fontSize);\r\n const singleLine = measureLine(text);\r\n const wrap =\r\n singleLine <= EDGE_LABEL_WRAP_WIDTH\r\n ? { lines: [text], truncated: false }\r\n : wrapTextMeasured(text, EDGE_LABEL_WRAP_WIDTH, measureLine, maxLines);\r\n const lines = wrap.lines.length > 0 ? wrap.lines : [text];\r\n let widest = 0;\r\n for (const line of lines) {\r\n widest = Math.max(widest, measureLine(line));\r\n }\r\n const width = Math.max(EDGE_LABEL_MIN_WIDTH, Math.ceil(widest) + EDGE_LABEL_PADDING_X * 2);\r\n const height = Math.ceil(lines.length * fontSize * LABEL_LINE_HEIGHT_EM) + EDGE_LABEL_PADDING_Y * 2;\r\n return { width, height, lines };\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Label placement */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Minimum wrap width (px) for external labels that hang off a small shape\r\n * (events, gateways). Without a floor, the budget collapses to the shape's\r\n * own width (~28px for an event) and ordinary words get hard-wrapped\r\n * character-by-character. ~96px keeps a ~14-char word (\"Undeliverable\") on a\r\n * single line and forces wraps onto word boundaries.\r\n */\r\nconst _EXTERNAL_LABEL_MIN_W = 96;\r\n\r\n/** Resolved placement metrics for a label. */\r\nexport interface LabelPlacement {\r\n x: number;\r\n y: number;\r\n textAnchor: 'start' | 'middle' | 'end';\r\n dominantBaseline: 'middle' | 'hanging' | 'auto';\r\n widthPx: number;\r\n}\r\n\r\n/**\r\n * Computes x/y/anchor for a label given the parent shape bbox and the\r\n * requested anchor region. `widthPx` is the available horizontal space\r\n * for wrapping.\r\n */\r\nexport function layoutLabel(opts: LabelOptions): LabelPlacement {\r\n const padding = opts.innerPadding ?? 8;\r\n\r\n // Author-encoded label bounds win over anchor heuristics. The `2` inset\r\n // guards against text clipping exactly at the author's box edge; real\r\n // text shapers add ~1-2px of ink padding.\r\n if (opts.labelBounds) {\r\n const b = opts.labelBounds;\r\n return {\r\n x: b.x + b.width / 2,\r\n y: b.y + b.height / 2,\r\n textAnchor: 'middle',\r\n dominantBaseline: 'middle',\r\n widthPx: Math.max(1, b.width - 2),\r\n };\r\n }\r\n\r\n const shape = opts.shape;\r\n switch (opts.anchor) {\r\n case 'inside':\r\n case 'center':\r\n if (opts.align === 'start') {\r\n return {\r\n x: shape.x + padding,\r\n y: shape.y + shape.height / 2,\r\n textAnchor: 'start',\r\n dominantBaseline: 'middle',\r\n widthPx: Math.max(1, shape.width - padding * 2),\r\n };\r\n }\r\n return {\r\n x: shape.x + shape.width / 2,\r\n y: shape.y + shape.height / 2,\r\n textAnchor: 'middle',\r\n dominantBaseline: 'middle',\r\n widthPx: Math.max(1, shape.width - padding * 2),\r\n };\r\n case 'below':\r\n return {\r\n x: shape.x + shape.width / 2,\r\n y: shape.y + shape.height + 6,\r\n textAnchor: 'middle',\r\n dominantBaseline: 'hanging',\r\n // External labels (events, gateways) attach to tiny shapes (~28px),\r\n // so shape.width * 1.6 yields a ~6-char budget that hard-wraps normal\r\n // words mid-character (\"Receiv\" / \"ed\"). Use a sensible minimum so\r\n // ordinary labels wrap at WORD boundaries. _EXTERNAL_LABEL_MIN_W\r\n // (~96px) holds a ~14-char word on one line.\r\n widthPx: Math.max(_EXTERNAL_LABEL_MIN_W, shape.width * 1.6),\r\n };\r\n case 'above':\r\n return {\r\n x: shape.x + shape.width / 2,\r\n y: shape.y - 6,\r\n textAnchor: 'middle',\r\n dominantBaseline: 'auto',\r\n widthPx: Math.max(_EXTERNAL_LABEL_MIN_W, shape.width * 1.6),\r\n };\r\n case 'left':\r\n return {\r\n x: shape.x - 6,\r\n y: shape.y + shape.height / 2,\r\n textAnchor: 'end',\r\n dominantBaseline: 'middle',\r\n widthPx: Math.max(1, shape.x),\r\n };\r\n case 'right':\r\n return {\r\n x: shape.x + shape.width + 6,\r\n y: shape.y + shape.height / 2,\r\n textAnchor: 'start',\r\n dominantBaseline: 'middle',\r\n widthPx: Math.max(1, shape.width),\r\n };\r\n default:\r\n return {\r\n x: shape.x + shape.width / 2,\r\n y: shape.y + shape.height / 2,\r\n textAnchor: 'middle',\r\n dominantBaseline: 'middle',\r\n widthPx: Math.max(1, shape.width - padding * 2),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Renders a fully-styled label SceneNode with word-wrap applied. The\r\n * caller provides shape geometry and anchor; this function does the\r\n * rest including vertical centering of multi-line text.\r\n */\r\nexport function renderLabel(opts: LabelOptions): SceneNode | null {\r\n if (opts.text.length === 0) {\r\n return null;\r\n }\r\n const fontSize = opts.fontSize ?? 12;\r\n // 0.55 is the measured average char-width factor for sans-serif mixed-case\r\n // at 12-16px (the classic \"0.6\" rule over-estimates and caused author-sized\r\n // ARIS / Bizagi labels to wrap one char short). Callers that need a real\r\n // text-metrics measurement can override via `charWidthFactor`.\r\n const charWidth = (opts.charWidthFactor ?? 0.55) * fontSize;\r\n const placement = layoutLabel(opts);\r\n // When the author provided BPMNLabel bounds, derive maxLines from the\r\n // authored height (the caller's `maxLines` acts as an upper bound only).\r\n // ARIS and friends size the label rect to exactly fit the intended line\r\n // count — trust that over a hard-coded default.\r\n const callerMax = opts.maxLines ?? 4;\r\n const lineHeightEm = 1.2;\r\n let effectiveMaxLines = callerMax;\r\n if (opts.labelBounds) {\r\n const fitLines = Math.max(1, Math.floor(opts.labelBounds.height / (fontSize * lineHeightEm)));\r\n effectiveMaxLines = Math.max(callerMax, fitLines);\r\n }\r\n const measureText = opts.measureText;\r\n const wrap =\r\n measureText !== undefined\r\n ? wrapTextMeasured(\r\n opts.text,\r\n placement.widthPx,\r\n (line) => measureText(line, fontSize),\r\n effectiveMaxLines,\r\n )\r\n : wrapText(opts.text, placement.widthPx, charWidth, effectiveMaxLines);\r\n if (wrap.lines.length === 0) {\r\n return null;\r\n }\r\n const tspans: TspanOptions[] = [];\r\n const lineHeight = lineHeightEm; // em\r\n // For vertical centering, shift the first baseline up so the whole block\r\n // is centred around `placement.y` when dominantBaseline is 'middle'.\r\n const totalHeight = (wrap.lines.length - 1) * lineHeight;\r\n const firstDy =\r\n placement.dominantBaseline === 'middle'\r\n ? `${(-totalHeight / 2).toFixed(3)}em`\r\n : placement.dominantBaseline === 'hanging'\r\n ? '0em'\r\n : '0em';\r\n for (let i = 0; i < wrap.lines.length; i += 1) {\r\n const line = wrap.lines[i] ?? '';\r\n if (i === 0) {\r\n tspans.push({ text: line, x: placement.x, dy: firstDy });\r\n } else {\r\n tspans.push({ text: line, x: placement.x, dy: `${lineHeight}em` });\r\n }\r\n }\r\n const textOpts: Parameters[0] = {\r\n x: placement.x,\r\n y: placement.y,\r\n text: opts.text,\r\n textAnchor: placement.textAnchor,\r\n dominantBaseline: placement.dominantBaseline,\r\n fontSize,\r\n fill: opts.fill ?? 'var(--mz-color-text-default)',\r\n className: opts.className ?? 'mz-label',\r\n tspans,\r\n };\r\n if (opts.fontFamily !== undefined) {\r\n textOpts.fontFamily = opts.fontFamily;\r\n }\r\n return makeText(textOpts);\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - edge separation + label placement (batch pass)\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: the auto-layout edge-quality pass (#7904). Two pure\r\n * functions, both batch-time only (never during interactive drags -\r\n * the Visio always-reroute jumpiness lesson):\r\n *\r\n * 1. separateEdges - libavoid-style nudging: when two flows run ON\r\n * TOP of each other (collinear overlapping segments), the\r\n * overlapping interior segments are nudged apart by a minimum\r\n * spacing so every line is individually readable. Segments that\r\n * touch a docking point (first/last) are pinned - docks are owned\r\n * by the anchor-distribution pass (#7898).\r\n *\r\n * 2. planEdgeLabelBounds - places each named flow's label off one of\r\n * its longest segments, choosing the first candidate that does not\r\n * collide with any flow segment, shape OR other label (\"the text\r\n * on the gateway is crossing with the line\" - NTT Data,\r\n * 2026-07-01). #8224 widened this from six fixed candidates scored\r\n * against a guessed box into a measured box searched over slide\r\n * positions and increasing perpendicular offsets, with labels\r\n * scored against each other.\r\n *\r\n * Pure module: plain records in, new geometry out. Callers own\r\n * command creation and undo.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { Point, Rect } from './geometry.js';\r\nimport { coalesceCollinear } from './geometry.js';\r\nimport {\r\n EDGE_LABEL_FONT_SIZE,\r\n EDGE_LABEL_MAX_LINES,\r\n measureEdgeLabelBox,\r\n} from '../renderers/labels.js';\r\nimport type { TextMeasurer } from '../text-metrics.js';\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Edge separation */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Edge record for {@link separateEdges}. */\r\nexport interface SeparableEdge {\r\n id: string;\r\n waypoints: readonly Point[];\r\n}\r\n\r\n/** Options for {@link separateEdges}. */\r\nexport interface SeparateOptions {\r\n /** Distance between nudged parallel segments. Default 8. */\r\n spacing?: number;\r\n /** Minimum shared run length that counts as an overlap. Default 8. */\r\n minOverlap?: number;\r\n}\r\n\r\ninterface SegmentRecord {\r\n edgeId: string;\r\n /** Index of the segment's first waypoint. */\r\n segIndex: number;\r\n horizontal: boolean;\r\n /** The shared perpendicular coordinate (y for horizontal segments). */\r\n coord: number;\r\n lo: number;\r\n hi: number;\r\n /** Segments touching a dock cannot move - the dock owns them. */\r\n pinned: boolean;\r\n}\r\n\r\n/**\r\n * Nudges overlapping collinear segments of DIFFERENT edges apart.\r\n * Returns a map of edge id to new waypoints for edges that changed.\r\n * Inputs are never mutated.\r\n */\r\nexport function separateEdges(\r\n edges: readonly SeparableEdge[],\r\n options: SeparateOptions = {},\r\n): Map {\r\n const spacing = options.spacing ?? 8;\r\n const minOverlap = options.minOverlap ?? 8;\r\n\r\n // Working copies keyed by edge id.\r\n const working = new Map();\r\n for (const edge of edges) {\r\n if (edge.waypoints.length < 2) continue;\r\n working.set(\r\n edge.id,\r\n edge.waypoints.map((p) => ({ x: p.x, y: p.y })),\r\n );\r\n }\r\n\r\n const segments: SegmentRecord[] = [];\r\n for (const [edgeId, wps] of working) {\r\n for (let i = 0; i < wps.length - 1; i += 1) {\r\n const a = wps[i];\r\n const b = wps[i + 1];\r\n if (!a || !b) continue;\r\n const horizontal = a.y === b.y && a.x !== b.x;\r\n const vertical = a.x === b.x && a.y !== b.y;\r\n if (!horizontal && !vertical) continue;\r\n segments.push({\r\n edgeId,\r\n segIndex: i,\r\n horizontal,\r\n coord: horizontal ? a.y : a.x,\r\n lo: horizontal ? Math.min(a.x, b.x) : Math.min(a.y, b.y),\r\n hi: horizontal ? Math.max(a.x, b.x) : Math.max(a.y, b.y),\r\n pinned: i === 0 || i + 1 === wps.length - 1,\r\n });\r\n }\r\n }\r\n\r\n // Group by axis, then cluster by near-equal coordinate (within 1\r\n // unit) and overlapping range.\r\n for (const horizontal of [true, false]) {\r\n const axisSegments = segments\r\n .filter((s) => s.horizontal === horizontal)\r\n .sort((a, b) => a.coord - b.coord || a.lo - b.lo);\r\n\r\n // Sweep into coordinate bands.\r\n let band: SegmentRecord[] = [];\r\n let bandCoord = Number.NEGATIVE_INFINITY;\r\n const bands: SegmentRecord[][] = [];\r\n for (const seg of axisSegments) {\r\n if (band.length === 0 || Math.abs(seg.coord - bandCoord) <= 1) {\r\n band.push(seg);\r\n bandCoord = seg.coord;\r\n } else {\r\n bands.push(band);\r\n band = [seg];\r\n bandCoord = seg.coord;\r\n }\r\n }\r\n if (band.length > 0) bands.push(band);\r\n\r\n for (const b of bands) {\r\n // Connected components of range-overlapping segments.\r\n const sorted = b.slice().sort((s, t) => s.lo - t.lo);\r\n let cluster: SegmentRecord[] = [];\r\n let clusterHi = Number.NEGATIVE_INFINITY;\r\n const flush = (): void => {\r\n if (cluster.length >= 2) {\r\n nudgeCluster(cluster, working, spacing);\r\n }\r\n cluster = [];\r\n clusterHi = Number.NEGATIVE_INFINITY;\r\n };\r\n for (const seg of sorted) {\r\n if (cluster.length === 0 || seg.lo <= clusterHi - minOverlap) {\r\n cluster.push(seg);\r\n clusterHi = Math.max(clusterHi, seg.hi);\r\n } else {\r\n flush();\r\n cluster.push(seg);\r\n clusterHi = seg.hi;\r\n }\r\n }\r\n flush();\r\n }\r\n }\r\n\r\n // Diff.\r\n const changed = new Map();\r\n for (const edge of edges) {\r\n const next = working.get(edge.id);\r\n if (!next) continue;\r\n const cleaned = coalesceCollinear(next);\r\n if (!pointsEqual(edge.waypoints, cleaned)) {\r\n changed.set(edge.id, cleaned);\r\n }\r\n }\r\n return changed;\r\n}\r\n\r\n/**\r\n * Spreads one overlap cluster apart. Pinned segments stay put (offset\r\n * 0); free segments take the nearest unused offsets in the pool\r\n * 0, +s, -s, +2s, -2s, ... Segments from the SAME edge only count once\r\n * toward distinctness - an edge overlapping itself is left alone.\r\n */\r\nfunction nudgeCluster(\r\n cluster: SegmentRecord[],\r\n working: Map,\r\n spacing: number,\r\n): void {\r\n const distinctEdges = new Set(cluster.map((s) => s.edgeId));\r\n if (distinctEdges.size < 2) return;\r\n\r\n const pinned = cluster.filter((s) => s.pinned);\r\n const free = cluster\r\n .filter((s) => !s.pinned)\r\n .sort((a, b) =>\r\n a.edgeId < b.edgeId ? -1 : a.edgeId > b.edgeId ? 1 : a.segIndex - b.segIndex,\r\n );\r\n if (free.length === 0) return;\r\n\r\n const used = new Set();\r\n if (pinned.length > 0) {\r\n used.add(0);\r\n }\r\n const pool: number[] = [0];\r\n for (let k = 1; k <= free.length + 1; k += 1) {\r\n pool.push(k * spacing, -k * spacing);\r\n }\r\n for (const seg of free) {\r\n const offset = pool.find((o) => !used.has(o));\r\n if (offset === undefined) break;\r\n used.add(offset);\r\n if (offset === 0) continue;\r\n const wps = working.get(seg.edgeId);\r\n if (!wps) continue;\r\n const a = wps[seg.segIndex];\r\n const b = wps[seg.segIndex + 1];\r\n if (!a || !b) continue;\r\n if (seg.horizontal) {\r\n wps[seg.segIndex] = { x: a.x, y: seg.coord + offset };\r\n wps[seg.segIndex + 1] = { x: b.x, y: seg.coord + offset };\r\n } else {\r\n wps[seg.segIndex] = { x: seg.coord + offset, y: a.y };\r\n wps[seg.segIndex + 1] = { x: seg.coord + offset, y: b.y };\r\n }\r\n }\r\n}\r\n\r\nfunction pointsEqual(a: readonly Point[], b: readonly Point[]): boolean {\r\n if (a.length !== b.length) return false;\r\n for (let i = 0; i < a.length; i += 1) {\r\n const pa = a[i];\r\n const pb = b[i];\r\n if (!pa || !pb || pa.x !== pb.x || pa.y !== pb.y) return false;\r\n }\r\n return true;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Label placement */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Edge record for {@link planEdgeLabelBounds}. */\r\nexport interface LabelPlanEdge {\r\n id: string;\r\n /** The flow's display name; unnamed flows get no label plan. */\r\n name?: string;\r\n waypoints: readonly Point[];\r\n}\r\n\r\n/** A planned label box. */\r\nexport interface PlannedLabelBounds {\r\n x: number;\r\n y: number;\r\n width: number;\r\n height: number;\r\n}\r\n\r\n/**\r\n * Perpendicular clearance between the line and the label box edge.\r\n *\r\n * 5, not 3: a connector is not just its polyline. The direction\r\n * chevrons (#7899) stand about 3.5 units off the line at a segment\r\n * midpoint, so a 3-unit gap let a label clip a chevron - and now that\r\n * labels paint above the connectors with a knockout plate behind them\r\n * (#8224), clipping it would rub the chevron out. The planner scores\r\n * against waypoints, so the gap itself has to cover the decoration.\r\n */\r\nconst LABEL_LINE_GAP = 5;\r\n\r\n/**\r\n * Extra perpendicular distances tried, in order, when the minimum gap\r\n * collides (#8224). Pushing a label further off its line is almost\r\n * always cheaper than accepting an overlap, and it keeps the label\r\n * unambiguously associated with the line it came off.\r\n */\r\n// The 48 was added by #8238: a gateway that sits immediately left of its\r\n// target task leaves a ~20px segment to hang the branch name off, and the\r\n// two shapes flanking it are 60px tall. Nothing at 32 or less clears both,\r\n// so the search had to settle for printing \"Yes\" on the task.\r\nconst LABEL_OFFSET_STEPS: readonly number[] = [0, 8, 18, 32, 48];\r\n\r\n/**\r\n * Fractions along a segment the label is slid to, in order (#8224). The\r\n * midpoint is the ideal; sliding towards either end usually clears a\r\n * crossing line or a neighbouring label without moving the label off\r\n * the segment it belongs to.\r\n */\r\nconst LABEL_SLIDE_POSITIONS: readonly number[] = [0.5, 0.35, 0.65, 0.22, 0.78];\r\n\r\n// A denser second sweep was tried here and REMOVED (#8238): eleven offset\r\n// steps and thirteen slide positions, run only when the coarse grid found\r\n// nowhere clean. It measured byte-identical on all seventeen fixtures - every\r\n// one of the ~1400 extra candidates collided too.\r\n//\r\n// Recorded because the reasoning that motivated it is seductive and wrong. A\r\n// free-grid probe found clean canvas ONE PIXEL from the \"Yes\" label on\r\n// five-lane-multi-return, which reads as a grid too coarse to see the gap\r\n// beside it. But a connector label may only hang off ITS OWN segments at a\r\n// perpendicular offset; a clean square of canvas the parameterisation cannot\r\n// reach is not a candidate that was missed. Measure the space the placer can\r\n// actually generate, not the space that exists.\r\n\r\n/** Shortest segment worth hanging a label off, when a longer one exists. */\r\nconst LABEL_MIN_SEGMENT = 20;\r\n\r\n/** How many of an edge's segments are considered, longest first. */\r\nconst LABEL_SEGMENT_BUDGET = 3;\r\n\r\n/** Score charged for landing on an opaque shape - the worst outcome. */\r\nconst SHAPE_PENALTY = 4;\r\n\r\n/** Score charged for landing on another label - two names, one spot. */\r\nconst LABEL_PENALTY = 3;\r\n\r\n/** Score charged per flow line the box crosses. */\r\nconst LINE_PENALTY = 1;\r\n\r\n/**\r\n * Score charged for straddling a lane divider or a pool border (#8238).\r\n *\r\n * Worse than crossing a flow line and better than landing on a shape. A\r\n * label sitting across a lane boundary does not just look untidy - in a\r\n * laned diagram it reads as belonging to neither lane, which is a claim\r\n * about the model rather than a blemish on it.\r\n */\r\nconst DIVIDER_PENALTY = 2;\r\n\r\n/**\r\n * Charged when a box leaves the lane band it belongs to (#8238).\r\n *\r\n * The worst outcome available to a placer, worse even than landing on a\r\n * shape: a note drawn in the wrong lane does not look untidy, it states\r\n * something the model does not say - that this remark belongs to that role.\r\n */\r\nconst OUT_OF_BAND_PENALTY = 12;\r\n\r\n/** Clearance demanded inside the band before a box counts as properly in it. */\r\nconst BAND_CLEARANCE = 5;\r\n\r\n/**\r\n * Charged for sitting inside the band but flush against its edge. Graded\r\n * rather than absolute on purpose: a genuinely tight lane may have nowhere\r\n * clean to go, and charging that at out-of-band rates would flatten the\r\n * search instead of guiding it.\r\n */\r\nconst BAND_EDGE_PENALTY = 3;\r\n\r\n/** True when `inner` lies wholly within `outer`. */\r\nfunction rectContains(outer: Rect, inner: PlannedLabelBounds): boolean {\r\n return (\r\n outer.x <= inner.x &&\r\n inner.x + inner.width <= outer.x + outer.width &&\r\n outer.y <= inner.y &&\r\n inner.y + inner.height <= outer.y + outer.height\r\n );\r\n}\r\n\r\n/** `r` shrunk on every side, floored so a thin band never inverts. */\r\nfunction insetRect(r: Rect, by: number): Rect {\r\n return {\r\n x: r.x + by,\r\n y: r.y + by,\r\n width: Math.max(1, r.width - by * 2),\r\n height: Math.max(1, r.height - by * 2),\r\n };\r\n}\r\n\r\n/** Options for {@link planEdgeLabelBounds}. */\r\nexport interface LabelPlanOptions {\r\n /**\r\n * Measures the rendered width of one line of label text. REQUIRED and\r\n * deliberately not defaulted: a character-count estimate is what made\r\n * the old planner score boxes nobody ever painted (#8224). Callers\r\n * with a DOM build one with `createSvgTextMeasurer`.\r\n */\r\n measureText: TextMeasurer;\r\n /**\r\n * Label boxes that are already fixed and must be treated as\r\n * obstacles - in practice the labels a person placed by hand (#8159),\r\n * which this pass may never move but must not plan on top of either.\r\n */\r\n fixedLabelBounds?: readonly PlannedLabelBounds[];\r\n /**\r\n * Lane dividers and pool borders, as line segments (#8238).\r\n *\r\n * These belong here rather than in `shapeBounds` because a lane band is\r\n * not opaque - every label in the diagram sits inside one. Passing the\r\n * bands as shapes makes EVERY candidate collide, which flattens the\r\n * search: no candidate can ever score 0, the ideal-first ordering stops\r\n * meaning anything, and the first box tried wins by default. Only the\r\n * band's edges are an obstacle, so only the edges are passed.\r\n */\r\n dividerSegments?: readonly { a: Point; b: Point }[];\r\n /**\r\n * Flow-line segments to avoid, for callers that are not planning from\r\n * a set of edges (#8247). `planEdgeLabelBounds` derives these from the\r\n * edges it is given; `planExternalLabelBounds` has no edges of its own\r\n * and must be handed the routes to dodge.\r\n */\r\n flowSegments?: readonly { a: Point; b: Point }[];\r\n /** Font size labels are drawn at. Defaults to the renderer's 11. */\r\n fontSize?: number;\r\n /** Wrapped-line budget. Defaults to the renderer's 3. */\r\n maxLines?: number;\r\n}\r\n\r\n/**\r\n * Plans a label box for every named edge (#7904, widened by #8224).\r\n *\r\n * For each edge the search starts at the ideal spot - the midpoint of\r\n * the longest segment, at the minimum gap off the line - and widens\r\n * from there: it slides the label ALONG the segment, then pushes it\r\n * further out perpendicular, then tries the other side, then the\r\n * runner-up segments. The first arrangement that collides with nothing\r\n * wins; if the diagram is crowded enough that everything collides, the\r\n * lowest-scoring box wins and the renderer's knockout keeps the text\r\n * legible.\r\n *\r\n * The collision universe is flow lines, shape boxes AND other labels -\r\n * both the ones this pass has already placed and any fixed (manually\r\n * placed) ones handed in via {@link LabelPlanOptions.fixedLabelBounds}.\r\n * Scoring labels against each other is what stops two names being\r\n * planned into the same square of canvas.\r\n *\r\n * Returns a map of edge id to planned bounds. Placement is sequential\r\n * and order-dependent by design: each label placed becomes an obstacle\r\n * for the ones after it, so the input order is the priority order.\r\n */\r\nexport function planEdgeLabelBounds(\r\n edges: readonly LabelPlanEdge[],\r\n shapeBounds: ReadonlyMap,\r\n options: LabelPlanOptions,\r\n): Map {\r\n // All segments across all edges - part of the collision universe.\r\n const allSegments: { a: Point; b: Point }[] = [];\r\n for (const edge of edges) {\r\n for (let i = 0; i < edge.waypoints.length - 1; i += 1) {\r\n const a = edge.waypoints[i];\r\n const b = edge.waypoints[i + 1];\r\n if (!a || !b) continue;\r\n allSegments.push({ a, b });\r\n }\r\n }\r\n const shapes = [...shapeBounds.values()];\r\n const dividers = options.dividerSegments ?? [];\r\n\r\n // Labels already committed: the pinned ones up front, then every box\r\n // this pass places, so no two labels are planned into one spot.\r\n const placedLabels: PlannedLabelBounds[] = [...(options.fixedLabelBounds ?? [])];\r\n\r\n const out = new Map();\r\n for (const edge of edges) {\r\n const name = edge.name ?? '';\r\n if (name.length === 0) continue;\r\n\r\n // Measured, wrap-aware footprint - the box that will be PAINTED,\r\n // not a character-count guess.\r\n const box = measureEdgeLabelBox(\r\n name,\r\n options.measureText,\r\n options.fontSize ?? EDGE_LABEL_FONT_SIZE,\r\n options.maxLines ?? EDGE_LABEL_MAX_LINES,\r\n );\r\n\r\n const segs = segmentsByLength(edge.waypoints);\r\n if (segs.length === 0) continue;\r\n\r\n let best: PlannedLabelBounds | undefined;\r\n let bestScore = Number.POSITIVE_INFINITY;\r\n\r\n /** One sweep of the candidate space at the given resolution. */\r\n const sweep = (\r\n offsetSteps: readonly number[],\r\n slidePositions: readonly number[],\r\n ): void => {\r\n search: for (const seg of segs) {\r\n const horizontal =\r\n Math.abs(seg.b.x - seg.a.x) >= Math.abs(seg.b.y - seg.a.y);\r\n for (const step of offsetSteps) {\r\n const gap = LABEL_LINE_GAP + step;\r\n for (const t of slidePositions) {\r\n const px = seg.a.x + (seg.b.x - seg.a.x) * t;\r\n const py = seg.a.y + (seg.b.y - seg.a.y) * t;\r\n for (const side of [1, -1]) {\r\n const candidate = horizontal\r\n ? {\r\n x: px - box.width / 2,\r\n y: side === 1 ? py - gap - box.height : py + gap,\r\n width: box.width,\r\n height: box.height,\r\n }\r\n : {\r\n x: side === 1 ? px + gap : px - gap - box.width,\r\n y: py - box.height / 2,\r\n width: box.width,\r\n height: box.height,\r\n };\r\n const score = scoreCandidate(\r\n candidate,\r\n allSegments,\r\n shapes,\r\n placedLabels,\r\n dividers,\r\n bestScore,\r\n );\r\n if (score < bestScore) {\r\n bestScore = score;\r\n best = candidate;\r\n // Clean spot found. Candidates are generated ideal-first,\r\n // so the first clean one is also the closest to ideal.\r\n if (score === 0) break search;\r\n }\r\n }\r\n }\r\n }\r\n }\r\n };\r\n\r\n sweep(LABEL_OFFSET_STEPS, LABEL_SLIDE_POSITIONS);\r\n\r\n if (best) {\r\n out.set(edge.id, best);\r\n placedLabels.push(best);\r\n }\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * An edge's segments, longest first, trimmed to the ones worth using.\r\n * Segments shorter than {@link LABEL_MIN_SEGMENT} are dropped - but\r\n * only while a longer one survives. A flow made entirely of stubs still\r\n * gets its longest segment, because placing the label slightly off a\r\n * short line beats the renderer's fallback of dropping it ON the line.\r\n */\r\nfunction segmentsByLength(\r\n waypoints: readonly Point[],\r\n): { a: Point; b: Point; length: number }[] {\r\n const segs: { a: Point; b: Point; length: number }[] = [];\r\n for (let i = 0; i < waypoints.length - 1; i += 1) {\r\n const a = waypoints[i];\r\n const b = waypoints[i + 1];\r\n if (!a || !b) continue;\r\n segs.push({ a, b, length: Math.hypot(b.x - a.x, b.y - a.y) });\r\n }\r\n segs.sort((s, t) => t.length - s.length);\r\n const usable = segs.filter((s) => s.length >= LABEL_MIN_SEGMENT);\r\n const chosen = usable.length > 0 ? usable : segs.slice(0, 1);\r\n return chosen.slice(0, LABEL_SEGMENT_BUDGET);\r\n}\r\n\r\n/**\r\n * Collision cost of a candidate box: 0 means it touches nothing. Shapes\r\n * are worst (an opaque box the text would sit on), other labels next\r\n * (two names in one place is unreadable either way round), lines\r\n * cheapest (a knockout behind the text still reads over a line).\r\n *\r\n * `budget` is the best score found so far; counting stops as soon as\r\n * the candidate can no longer beat it. The search tries up to 120\r\n * positions per label against every line, shape and placed label, so\r\n * abandoning a hopeless candidate early is what keeps a large diagram's\r\n * placement pass cheap.\r\n */\r\nfunction scoreCandidate(\r\n candidate: PlannedLabelBounds,\r\n segments: readonly { a: Point; b: Point }[],\r\n shapes: readonly Rect[],\r\n labels: readonly PlannedLabelBounds[],\r\n dividers: readonly { a: Point; b: Point }[],\r\n budget: number,\r\n band?: Rect,\r\n): number {\r\n let score = 0;\r\n if (band !== undefined) {\r\n if (!rectContains(band, candidate)) {\r\n score += OUT_OF_BAND_PENALTY;\r\n } else if (!rectContains(insetRect(band, BAND_CLEARANCE), candidate)) {\r\n // Inside the band but welded to its edge. Testing containment at zero\r\n // clearance makes a box resting exactly ON the lane line score nothing,\r\n // so the search stops there believing it is clean and never reaches the\r\n // genuinely clean candidate further down the list. A false zero does\r\n // not merely mis-rank - it truncates the search.\r\n score += BAND_EDGE_PENALTY;\r\n }\r\n if (score >= budget) return score;\r\n }\r\n for (const shape of shapes) {\r\n if (rectsOverlap(candidate, shape)) score += SHAPE_PENALTY;\r\n if (score >= budget) return score;\r\n }\r\n for (const label of labels) {\r\n if (rectsOverlap(candidate, label)) score += LABEL_PENALTY;\r\n if (score >= budget) return score;\r\n }\r\n for (const divider of dividers) {\r\n if (segmentIntersectsRect(divider.a, divider.b, candidate)) score += DIVIDER_PENALTY;\r\n if (score >= budget) return score;\r\n }\r\n for (const seg of segments) {\r\n if (segmentIntersectsRect(seg.a, seg.b, candidate)) score += LINE_PENALTY;\r\n if (score >= budget) return score;\r\n }\r\n return score;\r\n}\r\n\r\n/** True when segment ab passes through the rect's interior. */\r\nfunction segmentIntersectsRect(a: Point, b: Point, r: PlannedLabelBounds): boolean {\r\n const minX = Math.min(a.x, b.x);\r\n const maxX = Math.max(a.x, b.x);\r\n const minY = Math.min(a.y, b.y);\r\n const maxY = Math.max(a.y, b.y);\r\n // Fast reject via bounding boxes.\r\n if (maxX <= r.x || minX >= r.x + r.width) return false;\r\n if (maxY <= r.y || minY >= r.y + r.height) return false;\r\n // Axis-aligned segments (the normal case) are settled by the\r\n // bounding-box test above. For diagonals, sample the segment.\r\n if (a.x !== b.x && a.y !== b.y) {\r\n const steps = 8;\r\n for (let i = 0; i <= steps; i += 1) {\r\n const t = i / steps;\r\n const x = a.x + (b.x - a.x) * t;\r\n const y = a.y + (b.y - a.y) * t;\r\n if (x > r.x && x < r.x + r.width && y > r.y && y < r.y + r.height) {\r\n return true;\r\n }\r\n }\r\n return false;\r\n }\r\n return true;\r\n}\r\n\r\nfunction rectsOverlap(a: PlannedLabelBounds, b: Rect): boolean {\r\n return (\r\n a.x < b.x + b.width &&\r\n a.x + a.width > b.x &&\r\n a.y < b.y + b.height &&\r\n a.y + a.height > b.y\r\n );\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* External (shape) label placement */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Gap between a shape's edge and its external label box. */\r\nconst EXTERNAL_LABEL_GAP = 6;\r\n\r\n/** How far the search will push a label away from its shape before\r\n * giving up on that side and trying the next one. */\r\nconst EXTERNAL_PUSH_STEPS = [0, 10, 22, 36, 52];\r\n\r\n/** Sideways offsets tried along each side, as a fraction of the label\r\n * width. Centre first, then progressively further along the edge. */\r\nconst EXTERNAL_SLIDE_FRACTIONS = [0, -0.4, 0.4, -0.85, 0.85, -1.3, 1.3];\r\n\r\n/** Shape record for {@link planExternalLabelBounds}. */\r\nexport interface ExternalLabelShape {\r\n id: string;\r\n name: string;\r\n bounds: Rect;\r\n}\r\n\r\n/**\r\n * Place the external label of every gateway and event where nothing\r\n * else is, preferring inside the owning lane (#8247).\r\n *\r\n * These labels were positioned at a FIXED offset from the shape with no\r\n * collision check at all, so on any diagram the layout had rearranged\r\n * they landed on flow lines, on other boxes and across lane dividers.\r\n * Measured on the sample library before this: 35 collisions on the\r\n * editor engine, every one an external label rather than a connector\r\n * label - a distinction the `label-on-flow` category name hides.\r\n *\r\n * Candidate order encodes the convention: BELOW a start or end event\r\n * (where a name reads as a caption), ABOVE a gateway (where a question\r\n * reads as a prompt), then the other vertical side, then the two\r\n * horizontal ones. Within each side the box slides along the edge and\r\n * then pushes away from the shape. Ideal-first ordering means the first\r\n * candidate scoring zero is also the closest to convention, so the\r\n * search can stop the moment it finds clear space.\r\n *\r\n * Scoring is `scoreCandidate`, shared with the connector-label planner,\r\n * so both kinds of label agree on how bad each collision looks: a shape\r\n * worst, another label next, a lane divider next, a flow line cheapest.\r\n *\r\n * Lane dividers arrive as SEGMENTS, never as bands. Passing the band as\r\n * an opaque rectangle makes every candidate collide - no candidate can\r\n * reach zero, ideal-first ordering stops meaning anything, and whichever\r\n * box is tried first wins by default. That trap cost a day on #8238 and\r\n * is recorded in 8247's technical notes.\r\n *\r\n * Where a diagram is too crowded for a clean position the least-bad\r\n * candidate wins deterministically, by score and then by generation\r\n * order - never by chance.\r\n */\r\nexport function planExternalLabelBounds(\r\n shapes: readonly ExternalLabelShape[],\r\n obstacleShapes: readonly Rect[],\r\n options: LabelPlanOptions & { preferBelow?: (id: string) => boolean },\r\n): Map {\r\n const out = new Map();\r\n if (shapes.length === 0) return out;\r\n\r\n const dividers = options.dividerSegments ?? [];\r\n const segments = options.flowSegments ?? [];\r\n const placedLabels: PlannedLabelBounds[] = [...(options.fixedLabelBounds ?? [])];\r\n\r\n for (const shape of shapes) {\r\n const name = shape.name ?? '';\r\n if (name.length === 0) continue;\r\n\r\n const box = measureEdgeLabelBox(\r\n name,\r\n options.measureText,\r\n options.fontSize ?? EDGE_LABEL_FONT_SIZE,\r\n options.maxLines ?? EDGE_LABEL_MAX_LINES,\r\n );\r\n const w = box.width;\r\n const h = box.height;\r\n\r\n const cx = shape.bounds.x + shape.bounds.width / 2;\r\n const cy = shape.bounds.y + shape.bounds.height / 2;\r\n const below = options.preferBelow?.(shape.id) ?? true;\r\n\r\n // Vertical sides first, in convention order, then the horizontal\r\n // ones. A name beside a shape reads worse than one under it, so the\r\n // horizontal sides are a fallback rather than a peer.\r\n const sides: Array<'below' | 'above' | 'right' | 'left'> = below\r\n ? ['below', 'above', 'right', 'left']\r\n : ['above', 'below', 'right', 'left'];\r\n\r\n let best: PlannedLabelBounds | null = null;\r\n let bestScore = Number.POSITIVE_INFINITY;\r\n\r\n search: for (const side of sides) {\r\n for (const push of EXTERNAL_PUSH_STEPS) {\r\n for (const slide of EXTERNAL_SLIDE_FRACTIONS) {\r\n let x: number;\r\n let y: number;\r\n if (side === 'below' || side === 'above') {\r\n x = cx - w / 2 + slide * w;\r\n y =\r\n side === 'below'\r\n ? shape.bounds.y + shape.bounds.height + EXTERNAL_LABEL_GAP + push\r\n : shape.bounds.y - EXTERNAL_LABEL_GAP - h - push;\r\n } else {\r\n y = cy - h / 2 + slide * h;\r\n x =\r\n side === 'right'\r\n ? shape.bounds.x + shape.bounds.width + EXTERNAL_LABEL_GAP + push\r\n : shape.bounds.x - EXTERNAL_LABEL_GAP - w - push;\r\n }\r\n const candidate: PlannedLabelBounds = { x, y, width: w, height: h };\r\n const score = scoreCandidate(\r\n candidate,\r\n segments,\r\n obstacleShapes,\r\n placedLabels,\r\n dividers,\r\n bestScore,\r\n );\r\n if (score < bestScore) {\r\n bestScore = score;\r\n best = candidate;\r\n if (score === 0) break search;\r\n }\r\n }\r\n }\r\n }\r\n\r\n if (best) {\r\n out.set(shape.id, best);\r\n placedLabels.push(best);\r\n }\r\n }\r\n return out;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Text-annotation (note card) placement */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** A note card to be re-homed by {@link planAnnotationBounds}. */\r\nexport interface AnnotationCard {\r\n id: string;\r\n /** The card's CURRENT box. Its size is content-driven and is preserved;\r\n * only the position is planned. */\r\n bounds: Rect;\r\n /** The subject the note is associated with. A note with no association\r\n * is anchored to itself, so it stays where the author put it unless\r\n * that spot is dirty. */\r\n anchor: Rect;\r\n /** The lane band the subject sits in, when the diagram has lanes. A note\r\n * is kept inside it: a remark drawn in a lane its subject does not\r\n * belong to makes a false claim about the process. */\r\n band?: Rect;\r\n}\r\n\r\n/**\r\n * Re-home every text annotation onto clear canvas near its subject (#8238).\r\n *\r\n * Auto-layout rearranges the diagram around notes without ever moving them:\r\n * `textAnnotation` is not one of the types the layered pass places, so a card\r\n * keeps the geometry it arrived with while every shape and line around it\r\n * moves. It then reads as a remark about whatever has slid underneath it.\r\n *\r\n * Runs LAST, after both label passes, and the order is deliberate. A note is\r\n * the least constrained thing on the canvas - it may sit anywhere inside its\r\n * subject's lane - while an external label may only sit against its own\r\n * shape. Placing the free thing first lets it take the one strip the\r\n * constrained thing could have used; the same mistake, made in the Python\r\n * engine's pipeline, put an event's name on top of a note card and is\r\n * recorded on 8238. Constrained first, free last.\r\n *\r\n * The card's SIZE is never changed. It is measured from the note's text\r\n * upstream, and a placer that quietly reflowed someone's note to make it fit\r\n * would be editing content to satisfy geometry.\r\n *\r\n * Returns only the cards that actually move.\r\n */\r\nexport function planAnnotationBounds(\r\n cards: readonly AnnotationCard[],\r\n obstacleShapes: readonly Rect[],\r\n options: {\r\n fixedLabelBounds?: readonly PlannedLabelBounds[];\r\n dividerSegments?: readonly { a: Point; b: Point }[];\r\n flowSegments?: readonly { a: Point; b: Point }[];\r\n } = {},\r\n): Map {\r\n const out = new Map();\r\n if (cards.length === 0) return out;\r\n\r\n const dividers = options.dividerSegments ?? [];\r\n const segments = options.flowSegments ?? [];\r\n const placed: PlannedLabelBounds[] = [...(options.fixedLabelBounds ?? [])];\r\n\r\n // Biggest first: the hardest card to fit gets first pick of the clear\r\n // space rather than being left whatever the small ones did not want.\r\n const ordered = [...cards].sort(\r\n (a, b) =>\r\n b.bounds.width * b.bounds.height - a.bounds.width * a.bounds.height ||\r\n (a.id < b.id ? -1 : a.id > b.id ? 1 : 0),\r\n );\r\n\r\n for (let index = 0; index < ordered.length; index += 1) {\r\n const card = ordered[index] as AnnotationCard;\r\n const w = card.bounds.width;\r\n const h = card.bounds.height;\r\n const acx = card.anchor.x + card.anchor.width / 2;\r\n const acy = card.anchor.y + card.anchor.height / 2;\r\n\r\n // Cards not yet processed still occupy canvas. They are excluded from the\r\n // caller's shape obstacles (they are about to move) and are not in\r\n // `placed` (they have not been placed), so without this a card could take\r\n // a box another card is currently sitting in and starve it into an\r\n // overlap. Measured: two cards that started apart, the larger one moved\r\n // onto the smaller one's box, and the smaller then preferred that label\r\n // collision (3) to its shape-blocked candidates (4).\r\n //\r\n // KNOWN LIMITATION, recorded rather than fixed. This buys the overlap\r\n // fix with ORDER DEPENDENCE, because a pending card is an obstacle at a\r\n // position it is about to leave - the same \"planning against geometry\r\n // that is about to move\" this file fixes elsewhere for notes versus\r\n // labels, reintroduced here in a narrower place.\r\n //\r\n // Executed counterexample, by driving this planner directly rather than\r\n // from a document: with two cards, the larger processed first can find\r\n // every clean candidate blocked by the smaller card's CURRENT box, keep\r\n // its distant position, and only settle on a second call once the\r\n // smaller card has moved away. First pass 12,491px from its subject,\r\n // second pass 81px, third stable.\r\n //\r\n // NOT shown reachable from a real diagram: ordinary autoLayout\r\n // idempotence still holds by execution - full serialized XML identical\r\n // after a second layout, and a clear orphan note stays put across\r\n // repeated layouts with byte-identical output.\r\n //\r\n // The honest fix is not a fifth constraint on a single-pass greedy\r\n // placer. Each of the last four - current-box scoring, the strand guard,\r\n // the distant fallback, and this - bought one property by selling\r\n // another. It wants a settling pass: place every card once, then re-check\r\n // each against the others' FINAL positions, which removes the need to\r\n // guess about geometry that has not settled yet.\r\n const pending: Rect[] = [];\r\n for (let j = index + 1; j < ordered.length; j += 1) {\r\n const later = ordered[j];\r\n if (later) pending.push(later.bounds);\r\n }\r\n const shapesForThisCard =\r\n pending.length > 0 ? [...obstacleShapes, ...pending] : obstacleShapes;\r\n\r\n let best: PlannedLabelBounds | null = null;\r\n let bestScore = Number.POSITIVE_INFINITY;\r\n\r\n // Where the card already is, scored FIRST.\r\n //\r\n // Every generated candidate sits OUTSIDE the anchor, so a note with no\r\n // association - anchored to itself - could never be offered its own box\r\n // back. A perfectly clear orphan note was therefore re-placed on every\r\n // single Auto layout, stepping (900,900) -> (900,946) -> (900,992) and\r\n // dirtying the document each time. Auto layout must be idempotent on a\r\n // diagram it has nothing to say about.\r\n //\r\n // Scored, not exempted: a note sitting on a task still has to move.\r\n //\r\n // And only accepted early if it is somewhere this planner could plausibly\r\n // have PUT it. Keeping any clean box, however distant, traded churn for\r\n // strand: a note that arrives far from its subject - which is what happens\r\n // when everything around it has just been rearranged - sits on clear\r\n // canvas, scores zero, and is left stranded for good. Measured on the\r\n // sample library, notes ended up 457px and 573px from the thing they\r\n // annotate while the Python engine kept the same notes inside 140px.\r\n //\r\n // \"Within reach\" is defined by the generator itself rather than a\r\n // constant, so the two cannot drift apart: the furthest centre any\r\n // candidate below can reach. A note the planner placed is therefore always\r\n // within reach and stays put on a second run - idempotence holds - while\r\n // one dumped outside that envelope is re-placed properly.\r\n // The candidates, generated once, best-first. Generating them up front is\r\n // what makes `reach` EXACT rather than an estimate of itself: it is the\r\n // furthest centre this list actually contains.\r\n //\r\n // The previous formula added the slide and perpendicular offsets linearly\r\n // at full maximum dimensions and came out at roughly TWICE the real\r\n // envelope - 384px where the furthest real candidate was 189.7px - so\r\n // notes at 217px, 237px and 307px were all judged \"within reach\" and left\r\n // stranded. The constant was supposed to approximate the envelope; nobody\r\n // checked it against one.\r\n const candidates: PlannedLabelBounds[] = [];\r\n for (const push of EXTERNAL_PUSH_STEPS) {\r\n const gap = EXTERNAL_LABEL_GAP + push;\r\n for (const side of ['below', 'above', 'right', 'left'] as const) {\r\n for (const slide of EXTERNAL_SLIDE_FRACTIONS) {\r\n if (side === 'below' || side === 'above') {\r\n candidates.push({\r\n x: acx - w / 2 + slide * w,\r\n y:\r\n side === 'below'\r\n ? card.anchor.y + card.anchor.height + gap\r\n : card.anchor.y - gap - h,\r\n width: w,\r\n height: h,\r\n });\r\n } else {\r\n candidates.push({\r\n x:\r\n side === 'right'\r\n ? card.anchor.x + card.anchor.width + gap\r\n : card.anchor.x - gap - w,\r\n y: acy - h / 2 + slide * h,\r\n width: w,\r\n height: h,\r\n });\r\n }\r\n }\r\n }\r\n }\r\n let reach = 0;\r\n for (const c of candidates) {\r\n const d = Math.hypot(c.x + w / 2 - acx, c.y + h / 2 - acy);\r\n if (Number.isFinite(d) && d > reach) reach = d;\r\n }\r\n\r\n const current: PlannedLabelBounds = {\r\n x: card.bounds.x,\r\n y: card.bounds.y,\r\n width: w,\r\n height: h,\r\n };\r\n const currentDistance = Math.hypot(\r\n current.x + w / 2 - acx,\r\n current.y + h / 2 - acy,\r\n );\r\n bestScore = scoreCandidate(\r\n current,\r\n segments,\r\n shapesForThisCard,\r\n placed,\r\n dividers,\r\n bestScore,\r\n card.band,\r\n );\r\n best = current;\r\n // A distant box does not get to END the search, however clean it is - but\r\n // it is not thrown away either. If the search then finds nowhere clean\r\n // near the anchor, a clean box further off still beats a colliding one\r\n // nearby: strand is charged per pixel, a collision is a hard defect.\r\n const distantButClean = bestScore === 0 && currentDistance > reach;\r\n if (distantButClean) bestScore = Number.POSITIVE_INFINITY;\r\n\r\n for (const candidate of candidates) {\r\n if (bestScore === 0) break;\r\n const score = scoreCandidate(\r\n candidate,\r\n segments,\r\n shapesForThisCard,\r\n placed,\r\n dividers,\r\n bestScore,\r\n card.band,\r\n );\r\n if (score < bestScore) {\r\n bestScore = score;\r\n best = candidate;\r\n }\r\n }\r\n\r\n // Nowhere clean within reach, so the clean-but-distant box it started in\r\n // is still the best available.\r\n if (distantButClean && bestScore > 0) best = current;\r\n\r\n if (!best) continue;\r\n placed.push(best);\r\n // A card already in the best place is not a move. Emitting one anyway\r\n // would put \"Auto layout\" on the undo stack for a diagram nothing\r\n // changed in.\r\n if (\r\n Math.abs(best.x - card.bounds.x) < 0.5 &&\r\n Math.abs(best.y - card.bounds.y) < 0.5\r\n ) {\r\n continue;\r\n }\r\n out.set(card.id, best);\r\n }\r\n return out;\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - real text measurement\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: the measured half of #8224. Label placement used to guess\r\n * a label's width from `name.length * 6.5` and assume a single 14px-tall\r\n * line, while the renderer wrapped the same text to as many as three\r\n * lines. The collision box therefore described a rectangle nobody ever\r\n * painted - it invented collisions for short names and missed real ones\r\n * for long or wrapped names.\r\n *\r\n * The fix is to ask the browser. A hidden ``\r\n * is parked inside the SAME host element the diagram is mounted in, so\r\n * it inherits the identical font stack, size and letter-spacing the real\r\n * labels inherit, and `getComputedTextLength()` returns exactly the\r\n * advance width the renderer will paint.\r\n *\r\n * Environments without a text-measuring engine (jsdom under the unit\r\n * tests) are detected up front by {@link createSvgTextMeasurer}'s probe\r\n * and reported as `null` - a capability answer, not a swallowed failure.\r\n * Callers decide what to do about it; nothing here silently substitutes\r\n * a guess.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\n/** SVG namespace URI. */\r\nconst SVG_NS = 'http://www.w3.org/2000/svg';\r\n\r\n/**\r\n * Measures the rendered advance width, in user units, of ONE line of\r\n * label text at `fontSize`. Never called with a multi-line string -\r\n * wrapping is the caller's job (see `wrapTextMeasured`).\r\n */\r\nexport type TextMeasurer = (text: string, fontSize: number) => number;\r\n\r\n/** A live measurer plus the teardown for the DOM it parked. */\r\nexport interface TextMeasurerHandle {\r\n /** Measures one line of text. Results are memoised. */\r\n measure: TextMeasurer;\r\n /** Removes the hidden measuring SVG from the host. Idempotent. */\r\n dispose(): void;\r\n}\r\n\r\n/**\r\n * String used to probe whether the environment can measure text at all.\r\n * Ten wide glyphs: any real engine returns a comfortably positive width,\r\n * jsdom returns 0.\r\n */\r\nconst PROBE_TEXT = 'MMMMMMMMMM';\r\n\r\n/** Font size the probe measures at. Matches the edge-label size. */\r\nconst PROBE_FONT_SIZE = 11;\r\n\r\n/**\r\n * Creates a text measurer that measures against the same font context\r\n * as `host`. Returns `null` when the environment cannot measure text\r\n * (no document, or a `getComputedTextLength` that answers zero for a\r\n * ten-glyph string - i.e. jsdom).\r\n *\r\n * The caller owns the returned handle and MUST `dispose()` it; the\r\n * measuring SVG is inert (hidden, zero-sized, aria-hidden, no pointer\r\n * events) but it is still a DOM node parked in the consumer's element.\r\n *\r\n * @param host Element the diagram is mounted in; the measuring SVG\r\n * inherits its font so measurements match what is drawn.\r\n * @param className CSS class stamped on the probe ``, so any host\r\n * stylesheet targeting labels applies to it too.\r\n */\r\nexport function createSvgTextMeasurer(\r\n host: HTMLElement,\r\n className: string = 'mz-label',\r\n): TextMeasurerHandle | null {\r\n const ownerDocument = host.ownerDocument;\r\n if (!ownerDocument) return null;\r\n\r\n const svg = ownerDocument.createElementNS(SVG_NS, 'svg');\r\n svg.setAttribute('aria-hidden', 'true');\r\n svg.setAttribute('focusable', 'false');\r\n svg.setAttribute('width', '0');\r\n svg.setAttribute('height', '0');\r\n svg.setAttribute('data-mz-text-metrics', 'true');\r\n svg.setAttribute(\r\n 'style',\r\n 'position:absolute;width:0;height:0;overflow:hidden;visibility:hidden;pointer-events:none;',\r\n );\r\n const probe = ownerDocument.createElementNS(SVG_NS, 'text');\r\n probe.setAttribute('class', className);\r\n svg.appendChild(probe);\r\n host.appendChild(svg);\r\n\r\n let disposed = false;\r\n const dispose = (): void => {\r\n if (disposed) return;\r\n disposed = true;\r\n if (svg.parentNode) svg.parentNode.removeChild(svg);\r\n };\r\n\r\n const probeWidth = measureNode(probe, PROBE_TEXT, PROBE_FONT_SIZE);\r\n if (!Number.isFinite(probeWidth) || probeWidth <= 0) {\r\n dispose();\r\n return null;\r\n }\r\n\r\n const cache = new Map();\r\n const measure: TextMeasurer = (text, fontSize) => {\r\n if (disposed) return 0;\r\n if (text.length === 0) return 0;\r\n // `|` is an unambiguous separator here: the prefix is a number, whose\r\n // string form can only ever contain digits, `.`, `-`, `e` and `+`, so\r\n // the FIRST `|` always ends the size and everything after it is the\r\n // text verbatim. Distinct (size, text) pairs cannot collide.\r\n const key = `${fontSize}|${text}`;\r\n const cached = cache.get(key);\r\n if (cached !== undefined) return cached;\r\n const raw = measureNode(probe, text, fontSize);\r\n // The probe above proved this engine measures, so a non-finite\r\n // answer here is not a supported state - clamp to 0 rather than\r\n // propagate NaN into geometry, and let the caller's minimum-box\r\n // floor produce something drawable.\r\n const width = Number.isFinite(raw) && raw > 0 ? raw : 0;\r\n cache.set(key, width);\r\n return width;\r\n };\r\n\r\n return { measure, dispose };\r\n}\r\n\r\n/**\r\n * Average glyph advance as a fraction of font size for mixed-case\r\n * sans-serif Latin - the same 0.55 the label renderer has always used.\r\n */\r\nconst ESTIMATED_CHAR_WIDTH_FACTOR = 0.55;\r\n\r\n/**\r\n * A measurer for environments that have NO font engine to ask - a\r\n * headless test runner (jsdom), or a build step with no document.\r\n *\r\n * This is an estimate and is named as one. It is NOT a silent fallback\r\n * for the browser: {@link createSvgTextMeasurer} succeeds in every real\r\n * browser, so the rendered product always measures for real, and the\r\n * estimate only ever governs environments that paint nothing at all and\r\n * therefore have no label to lay on a line.\r\n *\r\n * Never reach for this in a browser path in preference to measuring.\r\n */\r\nexport function createEstimatedTextMeasurer(\r\n charWidthFactor: number = ESTIMATED_CHAR_WIDTH_FACTOR,\r\n): TextMeasurer {\r\n return (text, fontSize) => text.length * fontSize * charWidthFactor;\r\n}\r\n\r\n/** Sets the probe node's text and size, then reads its advance width. */\r\nfunction measureNode(probe: SVGTextElement, text: string, fontSize: number): number {\r\n probe.setAttribute('font-size', String(fontSize));\r\n probe.textContent = text;\r\n if (typeof probe.getComputedTextLength !== 'function') return 0;\r\n return probe.getComputedTextLength();\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - auto-layout Phase 1: cycle breaking\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * BPMN flows often loop (e.g. \"review fails -> back to edit\"). Pure\r\n * DAG-layering breaks on cycles. Standard fix is the greedy DFS\r\n * feedback-arc-set heuristic: walk the graph in DFS order, mark every\r\n * edge that points back to an ancestor on the current DFS stack as a\r\n * \"back edge\", and reverse it in the working graph. The resulting\r\n * graph is acyclic, so Phase 2 (layering) can do its job. The set of\r\n * reversed edges is returned so Phase 5 (routing) can render those\r\n * edges as loop-back curves rather than straight forward arrows.\r\n *\r\n * Complexity: O(V + E). One DFS pass.\r\n *\r\n * License: proprietary\r\n */\r\n\r\nexport interface DirectedEdge {\r\n source: string;\r\n target: string;\r\n}\r\n\r\nexport interface CycleBreakResult {\r\n /** Edge list with back edges flipped so the graph is a DAG. */\r\n acyclicEdges: DirectedEdge[];\r\n /** Original edge indices (into the input array) that were reversed. */\r\n reversedEdgeIndices: ReadonlySet;\r\n}\r\n\r\n/**\r\n * Break cycles in a directed graph by reversing back edges found via\r\n * DFS. Self-loops (source == target) are *removed* from the working\r\n * graph and reported as reversed; rendering them is the caller's\r\n * problem (typically a small circular arrow drawn next to the node).\r\n */\r\nexport function breakCycles(\r\n nodes: readonly string[],\r\n edges: readonly DirectedEdge[],\r\n): CycleBreakResult {\r\n // Build adjacency list with edge indices so we can mark which\r\n // original edge a discovered cycle corresponds to.\r\n const adj = new Map>();\r\n for (const id of nodes) adj.set(id, []);\r\n for (let i = 0; i < edges.length; i += 1) {\r\n const e = edges[i];\r\n if (!e) continue;\r\n const list = adj.get(e.source);\r\n if (list) list.push({ target: e.target, edgeIndex: i });\r\n }\r\n\r\n // DFS with three colours: white (unseen), grey (on current stack),\r\n // black (fully explored). Edges to a grey node are back edges.\r\n const WHITE = 0;\r\n const GREY = 1;\r\n const BLACK = 2;\r\n const colour = new Map();\r\n for (const id of nodes) colour.set(id, WHITE);\r\n const reversed = new Set();\r\n\r\n const visit = (start: string): void => {\r\n // Iterative DFS keyed by (nodeId, iterator index) so we don't blow\r\n // the call stack on long chains. Each stack entry advances through\r\n // the node's outgoing edges one at a time.\r\n const stack: Array<{ id: string; cursor: number }> = [\r\n { id: start, cursor: 0 },\r\n ];\r\n colour.set(start, GREY);\r\n while (stack.length > 0) {\r\n const frame = stack[stack.length - 1];\r\n if (!frame) break;\r\n const out = adj.get(frame.id);\r\n if (!out || frame.cursor >= out.length) {\r\n colour.set(frame.id, BLACK);\r\n stack.pop();\r\n continue;\r\n }\r\n const edge = out[frame.cursor];\r\n frame.cursor += 1;\r\n if (!edge) continue;\r\n const target = edge.target;\r\n if (target === frame.id) {\r\n // Self-loop: mark reversed; don't recurse.\r\n reversed.add(edge.edgeIndex);\r\n continue;\r\n }\r\n const targetColour = colour.get(target);\r\n if (targetColour === GREY) {\r\n // Back edge - reverse it.\r\n reversed.add(edge.edgeIndex);\r\n } else if (targetColour === WHITE) {\r\n colour.set(target, GREY);\r\n stack.push({ id: target, cursor: 0 });\r\n }\r\n // BLACK = forward / cross edge, no action needed.\r\n }\r\n };\r\n\r\n for (const id of nodes) {\r\n if (colour.get(id) === WHITE) visit(id);\r\n }\r\n\r\n // Build the acyclic edge list with back edges flipped. When the\r\n // reversed direction would duplicate an existing forward edge\r\n // (the 2-node a<->b case), keep just one - duplicates don't\r\n // change layering but produce confusing parallel layout columns.\r\n const acyclicEdges: DirectedEdge[] = [];\r\n const seen = new Set();\r\n const key = (s: string, t: string): string => s + '|' + t;\r\n for (let i = 0; i < edges.length; i += 1) {\r\n const e = edges[i];\r\n if (!e) continue;\r\n if (reversed.has(i)) {\r\n // Self-loops dropped from working graph (rendered separately).\r\n if (e.source === e.target) continue;\r\n const k = key(e.target, e.source);\r\n if (seen.has(k)) continue;\r\n seen.add(k);\r\n acyclicEdges.push({ source: e.target, target: e.source });\r\n } else {\r\n const k = key(e.source, e.target);\r\n if (seen.has(k)) continue;\r\n seen.add(k);\r\n acyclicEdges.push({ source: e.source, target: e.target });\r\n }\r\n }\r\n\r\n return { acyclicEdges, reversedEdgeIndices: reversed };\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - auto-layout Phase 3: crossing reduction\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Sugiyama Phase 3 reorders the nodes within each layer to minimise\r\n * the number of edge crossings between adjacent layers. This is the\r\n * single biggest visual win over the MVP - most \"spaghetti\" in\r\n * branching BPMN diagrams traces back to siblings being in the\r\n * wrong order within their layer.\r\n *\r\n * We use the **median heuristic** (Gansner et al. 1993):\r\n * for each node in layer L, compute the median index of its\r\n * neighbours in layer L-1; sort layer L by that median.\r\n * Empirical results show 4 down + 4 up sweeps capture ~95% of the\r\n * benefit of higher pass counts. We pick 8 sweeps total.\r\n *\r\n * Tie-breaking (when two nodes share the same median):\r\n * 1. Barycentre (mean of *all* neighbours rather than median).\r\n * 2. Original index (stable; isolated nodes stay put).\r\n *\r\n * Complexity: O(passes * V * E_max-per-node-in-layer).\r\n *\r\n * License: proprietary\r\n */\r\n\r\nimport type { DirectedEdge } from './cycle-break.js';\r\n\r\n/** Number of forward + backward sweeps. 8 = 4 down + 4 up. */\r\nconst DEFAULT_PASSES = 8;\r\n\r\n/**\r\n * Reorder each layer by median heuristic. Layer 0 (the source layer)\r\n * is the only layer not reordered against a previous one - it acts\r\n * as the \"fixed point\" the iteration anchors against. Layers 1..N-1\r\n * are reordered in down-sweeps; layers 0..N-2 in up-sweeps.\r\n *\r\n * The input `layers` is treated as an initial ordering (e.g. document\r\n * order from layering); the output is a new array of the same shape\r\n * with nodes reordered within each layer.\r\n */\r\nexport function reduceCrossings(\r\n layers: ReadonlyArray,\r\n edges: readonly DirectedEdge[],\r\n passes: number = DEFAULT_PASSES,\r\n): string[][] {\r\n if (layers.length < 2) {\r\n return layers.map((l) => [...l]);\r\n }\r\n\r\n // Capture original layer indices for stable tie-break.\r\n const originalIndex = new Map();\r\n for (const layer of layers) {\r\n for (let i = 0; i < layer.length; i += 1) {\r\n const id = layer[i];\r\n if (id !== undefined) originalIndex.set(id, i);\r\n }\r\n }\r\n\r\n // Pre-build adjacency: for each node, its neighbours in the\r\n // previous layer (incoming) and the next layer (outgoing).\r\n // Storing them as id sets lets us find their position in the\r\n // *current* ordering on every pass without mutating the structure.\r\n const incoming = new Map();\r\n const outgoing = new Map();\r\n for (const e of edges) {\r\n let outList = outgoing.get(e.source);\r\n if (!outList) {\r\n outList = [];\r\n outgoing.set(e.source, outList);\r\n }\r\n outList.push(e.target);\r\n let inList = incoming.get(e.target);\r\n if (!inList) {\r\n inList = [];\r\n incoming.set(e.target, inList);\r\n }\r\n inList.push(e.source);\r\n }\r\n\r\n // Mutable working copy.\r\n let working: string[][] = layers.map((l) => [...l]);\r\n\r\n for (let pass = 0; pass < passes; pass += 1) {\r\n const downward = pass % 2 === 0;\r\n if (downward) {\r\n for (let li = 1; li < working.length; li += 1) {\r\n working[li] = orderByMedian(\r\n working[li] ?? [],\r\n working[li - 1] ?? [],\r\n incoming,\r\n originalIndex,\r\n );\r\n }\r\n } else {\r\n for (let li = working.length - 2; li >= 0; li -= 1) {\r\n working[li] = orderByMedian(\r\n working[li] ?? [],\r\n working[li + 1] ?? [],\r\n outgoing,\r\n originalIndex,\r\n );\r\n }\r\n }\r\n }\r\n return working;\r\n}\r\n\r\n/**\r\n * Reorder `layer` by the median position-in-`refLayer` of each\r\n * node's neighbours. `neighboursOf(id)` returns the relevant set\r\n * (incoming for down-sweep, outgoing for up-sweep).\r\n */\r\nfunction orderByMedian(\r\n layer: readonly string[],\r\n refLayer: readonly string[],\r\n neighboursOf: ReadonlyMap,\r\n originalIndex: ReadonlyMap,\r\n): string[] {\r\n if (layer.length <= 1) return [...layer];\r\n\r\n const refIndex = new Map();\r\n for (let i = 0; i < refLayer.length; i += 1) {\r\n const id = refLayer[i];\r\n if (id !== undefined) refIndex.set(id, i);\r\n }\r\n\r\n // Compute (median, barycentre) for each node. Nodes with no\r\n // neighbours in refLayer keep -1 sentinel for both.\r\n interface ScoredNode {\r\n id: string;\r\n median: number;\r\n barycentre: number;\r\n isolated: boolean;\r\n }\r\n const scored: ScoredNode[] = layer.map((id) => {\r\n const nbrs = neighboursOf.get(id) ?? [];\r\n const positions: number[] = [];\r\n for (const n of nbrs) {\r\n const p = refIndex.get(n);\r\n if (p !== undefined) positions.push(p);\r\n }\r\n if (positions.length === 0) {\r\n return { id, median: -1, barycentre: -1, isolated: true };\r\n }\r\n positions.sort((a, b) => a - b);\r\n const median = computeMedian(positions);\r\n const barycentre =\r\n positions.reduce((acc, n) => acc + n, 0) / positions.length;\r\n return { id, median, barycentre, isolated: false };\r\n });\r\n\r\n scored.sort((a, b) => {\r\n // Isolated nodes sort to the end (preserving their relative\r\n // original order).\r\n if (a.isolated && b.isolated) {\r\n return (\r\n (originalIndex.get(a.id) ?? 0) - (originalIndex.get(b.id) ?? 0)\r\n );\r\n }\r\n if (a.isolated) return 1;\r\n if (b.isolated) return -1;\r\n if (a.median !== b.median) return a.median - b.median;\r\n if (a.barycentre !== b.barycentre) return a.barycentre - b.barycentre;\r\n return (\r\n (originalIndex.get(a.id) ?? 0) - (originalIndex.get(b.id) ?? 0)\r\n );\r\n });\r\n\r\n return scored.map((s) => s.id);\r\n}\r\n\r\n/**\r\n * Median of a sorted ascending list. For even-length lists we\r\n * return the *mean* of the two middle values (Gansner et al.) -\r\n * this gives crossing reduction better tie-resistance than picking\r\n * either of the two centres.\r\n */\r\nfunction computeMedian(sorted: readonly number[]): number {\r\n const n = sorted.length;\r\n if (n === 0) return -1;\r\n if (n % 2 === 1) {\r\n return sorted[Math.floor(n / 2)] ?? 0;\r\n }\r\n const mid = n / 2;\r\n return ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2;\r\n}\r\n\r\n/**\r\n * Count the number of edge crossings between two adjacent layers,\r\n * given a fixed ordering. Useful for tests that need to verify the\r\n * heuristic actually reduced crossings.\r\n *\r\n * Two edges (u1, v1) and (u2, v2) cross iff\r\n * (pos[u1] - pos[u2]) and (pos[v1] - pos[v2]) have opposite signs.\r\n */\r\nexport function countCrossings(\r\n upperLayer: readonly string[],\r\n lowerLayer: readonly string[],\r\n edges: readonly DirectedEdge[],\r\n): number {\r\n const upperPos = new Map();\r\n for (let i = 0; i < upperLayer.length; i += 1) {\r\n const id = upperLayer[i];\r\n if (id !== undefined) upperPos.set(id, i);\r\n }\r\n const lowerPos = new Map();\r\n for (let i = 0; i < lowerLayer.length; i += 1) {\r\n const id = lowerLayer[i];\r\n if (id !== undefined) lowerPos.set(id, i);\r\n }\r\n const relevant: Array<{ u: number; v: number }> = [];\r\n for (const e of edges) {\r\n const u = upperPos.get(e.source);\r\n const v = lowerPos.get(e.target);\r\n if (u === undefined || v === undefined) continue;\r\n relevant.push({ u, v });\r\n }\r\n let count = 0;\r\n for (let i = 0; i < relevant.length; i += 1) {\r\n for (let j = i + 1; j < relevant.length; j += 1) {\r\n const a = relevant[i];\r\n const b = relevant[j];\r\n if (!a || !b) continue;\r\n if ((a.u - b.u) * (a.v - b.v) < 0) count += 1;\r\n }\r\n }\r\n return count;\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - auto-layout Phase 4: Y-coordinate assignment\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Once Phase 3 has settled the within-layer ordering, Phase 4 picks\r\n * actual Y coordinates for every node. We use a simplified\r\n * Brandes-Köpf approach (research-confirmed to capture ~80% of the\r\n * visual quality of the full algorithm at ~10% of the code):\r\n *\r\n * 1. Find the longest start->end path through the graph - the\r\n * \"trunk\". Trunk nodes get pinned to the lane's vertical centre\r\n * so the happy path stays in the middle of the diagram.\r\n * 2. For every other node, compute the median Y of its incoming\r\n * neighbours and use that to seed its position within its layer.\r\n * 3. Within each lane (or, no-lane, the pool body), distribute the\r\n * layer's nodes vertically into equal stripes around the trunk.\r\n *\r\n * Lane awareness (hard constraint, work item #7566): a node never\r\n * leaves the lane it was assigned to. The caller is responsible for\r\n * sizing each lane to fit its content (see computeRequiredLaneHeights)\r\n * and hands placeY the resulting lane rectangles; placeY then clamps\r\n * EVERY node into its lane's vertical domain so nothing renders outside\r\n * its lane or pool. Lane containment wins over aesthetic centring.\r\n *\r\n * License: proprietary\r\n */\r\n\r\nimport type { DirectedEdge } from './cycle-break.js';\r\n\r\nexport type BBox = { x: number; y: number; width: number; height: number };\r\n\r\nexport interface PlacementNode {\r\n id: string;\r\n /** Pre-existing bounds; we copy width/height into the result. */\r\n bounds: BBox;\r\n /** Layer index from Phase 2. */\r\n layer: number;\r\n /** Index within the layer from Phase 3 (0 = top). Used as a\r\n * fallback ordering when neighbours don't disambiguate. */\r\n layerOrder: number;\r\n /** Optional: which lane this node belongs to (by id). null when\r\n * the diagram has no lanes. */\r\n laneId: string | null;\r\n}\r\n\r\nexport interface LaneRect {\r\n id: string;\r\n bounds: BBox;\r\n}\r\n\r\nexport interface PlacementInput {\r\n nodes: readonly PlacementNode[];\r\n edges: readonly DirectedEdge[];\r\n /** Lanes the layout must respect. Empty when the diagram has no\r\n * lane structure (in which case the pool body acts as the single\r\n * vertical domain). */\r\n lanes: readonly LaneRect[];\r\n /** Pool body bounds (used when there are no lanes). null when\r\n * laying out a pool-less document. */\r\n poolBounds: BBox | null;\r\n /** Anchor for the leftmost column. */\r\n xOrigin: number;\r\n /** Distance between columns. */\r\n columnWidth: number;\r\n /** Vertical separation between siblings in the same layer-lane\r\n * cell when more than one node shares it. */\r\n rowGap: number;\r\n}\r\n\r\nexport interface PlacementResult {\r\n /** New bounds for every node we placed. */\r\n bounds: Map;\r\n}\r\n\r\n/**\r\n * Compute new bounds for every flow node. Trunk centring runs first;\r\n * remaining nodes flow into stripes around the trunk within each\r\n * lane.\r\n */\r\nexport function placeY(input: PlacementInput): PlacementResult {\r\n const { nodes, edges, lanes, poolBounds, xOrigin, columnWidth, rowGap } =\r\n input;\r\n\r\n if (nodes.length === 0) {\r\n return { bounds: new Map() };\r\n }\r\n\r\n // Identify start nodes (no incoming) and end nodes (no outgoing)\r\n // for trunk detection.\r\n const incoming = new Map();\r\n const outgoing = new Map();\r\n for (const n of nodes) {\r\n incoming.set(n.id, []);\r\n outgoing.set(n.id, []);\r\n }\r\n for (const e of edges) {\r\n incoming.get(e.target)?.push(e.source);\r\n outgoing.get(e.source)?.push(e.target);\r\n }\r\n\r\n const trunk = computeTrunk(nodes, incoming, outgoing);\r\n\r\n // Group nodes by (layer, laneId) cell. Within each cell we'll\r\n // assign Y stripes.\r\n const cells = new Map();\r\n const cellKey = (layer: number, laneId: string | null): string =>\r\n `${String(layer)}::${laneId ?? '__nolane'}`;\r\n for (const n of nodes) {\r\n const key = cellKey(n.layer, n.laneId);\r\n let bucket = cells.get(key);\r\n if (!bucket) {\r\n bucket = [];\r\n cells.set(key, bucket);\r\n }\r\n bucket.push(n);\r\n }\r\n\r\n // Sort each cell by Phase-3 layerOrder so trunk stays centred and\r\n // sibling order is stable.\r\n for (const bucket of cells.values()) {\r\n bucket.sort((a, b) => a.layerOrder - b.layerOrder);\r\n }\r\n\r\n const lanesById = new Map();\r\n for (const l of lanes) lanesById.set(l.id, l);\r\n\r\n const result = new Map();\r\n for (const [, bucket] of cells) {\r\n const first = bucket[0];\r\n if (!first) continue;\r\n const lane = first.laneId ? lanesById.get(first.laneId) ?? null : null;\r\n // Pool-less fallback: synthesise a vertical domain large enough\r\n // to fit every node in the bucket with row-gap spacing. Without\r\n // this, branches at the same layer would all collide at y=0.\r\n const synthesisedDomainHeight = bucket.reduce(\r\n (sum, n) => sum + n.bounds.height + rowGap,\r\n 0,\r\n );\r\n const verticalDomain: { yTop: number; yBottom: number } = lane\r\n ? { yTop: lane.bounds.y, yBottom: lane.bounds.y + lane.bounds.height }\r\n : poolBounds\r\n ? {\r\n yTop: poolBounds.y,\r\n yBottom: poolBounds.y + poolBounds.height,\r\n }\r\n : { yTop: 0, yBottom: Math.max(synthesisedDomainHeight, 200) };\r\n\r\n distributeCellY(bucket, verticalDomain, trunk, rowGap, result, {\r\n xOrigin,\r\n columnWidth,\r\n });\r\n }\r\n\r\n return { bounds: result };\r\n}\r\n\r\n/**\r\n * Compute the longest path from any source (no-incoming) to any\r\n * sink (no-outgoing) by length. Returned set contains every node\r\n * on that path. Empty when the graph has no source or no sink.\r\n *\r\n * O(V + E). Layered structure is implicit - we use the existing\r\n * topological order from the layer assignment by visiting nodes\r\n * in increasing layer order.\r\n */\r\nfunction computeTrunk(\r\n nodes: readonly PlacementNode[],\r\n incoming: ReadonlyMap,\r\n outgoing: ReadonlyMap,\r\n): ReadonlySet {\r\n if (nodes.length === 0) return new Set();\r\n // longest-path-to-here, with predecessor pointer for backtracking.\r\n const dist = new Map();\r\n const prev = new Map();\r\n const order = [...nodes].sort((a, b) => a.layer - b.layer);\r\n for (const n of order) {\r\n const incomingNodes = incoming.get(n.id) ?? [];\r\n const isSource = incomingNodes.length === 0;\r\n if (isSource) {\r\n dist.set(n.id, 0);\r\n prev.set(n.id, null);\r\n continue;\r\n }\r\n let best = -Infinity;\r\n let bestPred: string | null = null;\r\n for (const p of incomingNodes) {\r\n const pd = dist.get(p);\r\n if (pd === undefined) continue;\r\n if (pd + 1 > best) {\r\n best = pd + 1;\r\n bestPred = p;\r\n }\r\n }\r\n dist.set(n.id, best === -Infinity ? 0 : best);\r\n prev.set(n.id, bestPred);\r\n }\r\n\r\n // Pick the sink (no-outgoing) with the largest distance.\r\n let trunkSink: string | null = null;\r\n let trunkLen = -1;\r\n for (const n of nodes) {\r\n const outgoingNodes = outgoing.get(n.id) ?? [];\r\n if (outgoingNodes.length !== 0) continue;\r\n const d = dist.get(n.id) ?? 0;\r\n if (d > trunkLen) {\r\n trunkLen = d;\r\n trunkSink = n.id;\r\n }\r\n }\r\n\r\n // Walk back to the source.\r\n const trunk = new Set();\r\n let cursor: string | null = trunkSink;\r\n while (cursor) {\r\n trunk.add(cursor);\r\n cursor = prev.get(cursor) ?? null;\r\n }\r\n return trunk;\r\n}\r\n\r\n/**\r\n * Distribute a single (layer, lane) cell's nodes across the lane's\r\n * vertical domain. Trunk nodes pinned to centre; non-trunk nodes\r\n * fill remaining space in equal stripes above and below.\r\n */\r\nfunction distributeCellY(\r\n bucket: readonly PlacementNode[],\r\n domain: { yTop: number; yBottom: number },\r\n trunk: ReadonlySet,\r\n rowGap: number,\r\n out: Map,\r\n xConfig: { xOrigin: number; columnWidth: number },\r\n): void {\r\n const { xOrigin, columnWidth } = xConfig;\r\n const domainHeight = domain.yBottom - domain.yTop;\r\n const centreY = domain.yTop + domainHeight / 2;\r\n\r\n const trunkNodes = bucket.filter((n) => trunk.has(n.id));\r\n const nonTrunk = bucket.filter((n) => !trunk.has(n.id));\r\n\r\n // Determine how many stripes we need: trunk + non-trunk siblings.\r\n const stripeCount = Math.max(\r\n bucket.length,\r\n trunkNodes.length === 1 && nonTrunk.length > 0\r\n ? nonTrunk.length + 1\r\n : bucket.length,\r\n );\r\n\r\n // If exactly one trunk node and any non-trunk, alternate above/below.\r\n // Otherwise distribute as equal stripes top-to-bottom.\r\n if (trunkNodes.length === 1 && nonTrunk.length > 0) {\r\n const trunkNode = trunkNodes[0];\r\n if (!trunkNode) return;\r\n // Clamp even the trunk node: a lane shorter than the node would\r\n // otherwise let it spill past the lane's top/bottom edge.\r\n const trunkY = clampY(\r\n centreY - trunkNode.bounds.height / 2,\r\n trunkNode.bounds.height,\r\n domain,\r\n );\r\n setBoundsForNodeAbsoluteY(trunkNode, trunkY, out, xOrigin, columnWidth);\r\n // Place non-trunk siblings above and below in alternating rows.\r\n let above = 0;\r\n let below = 0;\r\n for (let i = 0; i < nonTrunk.length; i += 1) {\r\n const node = nonTrunk[i];\r\n if (!node) continue;\r\n const offset = i % 2 === 0 ? (above += 1) * -1 : (below += 1);\r\n const targetY =\r\n centreY +\r\n offset * (Math.max(node.bounds.height, 60) + rowGap) -\r\n node.bounds.height / 2;\r\n const clamped = clampY(targetY, node.bounds.height, domain);\r\n setBoundsForNodeAbsoluteY(\r\n node,\r\n clamped,\r\n out,\r\n xOrigin,\r\n columnWidth,\r\n );\r\n }\r\n return;\r\n }\r\n\r\n // Default: equal stripes top to bottom in cell order.\r\n const total = bucket.length;\r\n if (total === 0) return;\r\n if (total === 1) {\r\n const node = bucket[0];\r\n if (!node) return;\r\n const onlyY = clampY(\r\n centreY - node.bounds.height / 2,\r\n node.bounds.height,\r\n domain,\r\n );\r\n setBoundsForNodeAbsoluteY(node, onlyY, out, xOrigin, columnWidth);\r\n return;\r\n }\r\n const stripeHeight = domainHeight / total;\r\n for (let i = 0; i < total; i += 1) {\r\n const node = bucket[i];\r\n if (!node) continue;\r\n const stripeCentre = domain.yTop + (i + 0.5) * stripeHeight;\r\n // Clamp so a stripe taller than the lane can't push a node past\r\n // the lane edge (lane containment is a hard constraint).\r\n const stripeY = clampY(\r\n stripeCentre - node.bounds.height / 2,\r\n node.bounds.height,\r\n domain,\r\n );\r\n setBoundsForNodeAbsoluteY(node, stripeY, out, xOrigin, columnWidth);\r\n }\r\n // Suppress unused-variable warning for `rowGap` and `stripeCount`\r\n // when neither branch above used them.\r\n void rowGap;\r\n void stripeCount;\r\n}\r\n\r\nfunction setBoundsForNodeAbsoluteY(\r\n node: PlacementNode,\r\n y: number,\r\n out: Map,\r\n xOrigin: number,\r\n columnWidth: number,\r\n): void {\r\n out.set(node.id, {\r\n x: xOrigin + node.layer * columnWidth,\r\n y,\r\n width: node.bounds.width,\r\n height: node.bounds.height,\r\n });\r\n}\r\n\r\nfunction clampY(\r\n y: number,\r\n height: number,\r\n domain: { yTop: number; yBottom: number },\r\n): number {\r\n const minY = domain.yTop;\r\n const maxY = domain.yBottom - height;\r\n if (maxY <= minY) return minY;\r\n return Math.max(minY, Math.min(maxY, y));\r\n}\r\n\r\n/** Vertical breathing room kept above and below the stacked content\r\n * inside a lane, so shapes don't touch the lane border. */\r\nexport const LANE_VERTICAL_PADDING = 20;\r\n\r\n/**\r\n * Compute the minimum height each lane needs to hold the nodes assigned\r\n * to it without any two overlapping. A lane's required height is driven\r\n * by its most crowded layer: the (lane, layer) cell with the greatest\r\n * total node height + inter-row gaps. The pool/lane stacking pass uses\r\n * this to grow lanes so the layout never has to spill a node outside its\r\n * lane (work item #7566, AC 2 + 3).\r\n *\r\n * `nodes` whose `laneId` is null are ignored - they belong to a pool\r\n * body with no lane structure and are sized by the pool, not a lane.\r\n *\r\n * Returns a map of laneId -> required height (>= minLaneHeight). Lanes\r\n * with no assigned nodes get `minLaneHeight`.\r\n */\r\nexport function computeRequiredLaneHeights(\r\n nodes: readonly PlacementNode[],\r\n laneIds: readonly string[],\r\n rowGap: number,\r\n minLaneHeight: number,\r\n): Map {\r\n // Sum of node heights + gaps per (lane, layer) cell.\r\n const cellHeight = new Map();\r\n const cellKey = (laneId: string, layer: number): string =>\r\n `${laneId}::${String(layer)}`;\r\n for (const node of nodes) {\r\n if (node.laneId === null) continue;\r\n const key = cellKey(node.laneId, node.layer);\r\n const running = cellHeight.get(key) ?? 0;\r\n const addition = node.bounds.height + (running > 0 ? rowGap : 0);\r\n cellHeight.set(key, running + addition);\r\n }\r\n\r\n const required = new Map();\r\n for (const laneId of laneIds) required.set(laneId, minLaneHeight);\r\n for (const [key, stacked] of cellHeight) {\r\n const laneId = key.slice(0, key.lastIndexOf('::'));\r\n const needed = stacked + 2 * LANE_VERTICAL_PADDING;\r\n const current = required.get(laneId) ?? minLaneHeight;\r\n if (needed > current) required.set(laneId, needed);\r\n }\r\n return required;\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - auto-layout module\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: research-grade Sugiyama-style layered layout for BPMN\r\n * diagrams. Currently this file holds the MVP (Phase 2 only - longest-\r\n * path layering + lane-aware Y centring), exactly as it shipped in\r\n * BpmnEditor.ts. Subsequent commits will split this file into one\r\n * file per Sugiyama phase:\r\n *\r\n * - cycle-break.ts Phase 1 (DFS feedback-arc-set)\r\n * - layering.ts Phase 2 (longest-path; existing code)\r\n * - dummies.ts Phase 2.5 (long-edge dummy nodes)\r\n * - crossing-reduction.ts Phase 3 (median heuristic, 8 sweeps)\r\n * - y-placement.ts Phase 4 (simplified Brandes-Köpf)\r\n * - routing.ts Phase 5 (orthogonal edge routing)\r\n *\r\n * For now the public surface is `runAutoLayout(doc, deps)`, which\r\n * mirrors the behaviour the BpmnEditor facade had inline.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { BpmnDocument, BpmnElement } from '../../model/index.js';\r\nimport { breakCycles } from './cycle-break.js';\r\nimport { reduceCrossings } from './crossing-reduction.js';\r\nimport {\r\n placeY,\r\n computeRequiredLaneHeights,\r\n type PlacementNode,\r\n} from './y-placement.js';\r\n\r\n/** Element types that participate in the longest-path layered layout. */\r\nexport const FLOW_NODE_TYPES_FOR_LAYOUT: ReadonlySet = new Set([\r\n 'startEvent',\r\n 'endEvent',\r\n 'intermediateThrowEvent',\r\n 'intermediateCatchEvent',\r\n 'boundaryEvent',\r\n 'task',\r\n 'userTask',\r\n 'serviceTask',\r\n 'sendTask',\r\n 'receiveTask',\r\n 'manualTask',\r\n 'scriptTask',\r\n 'businessRuleTask',\r\n 'callActivity',\r\n 'subProcess',\r\n 'adHocSubProcess',\r\n 'transaction',\r\n 'exclusiveGateway',\r\n 'parallelGateway',\r\n 'inclusiveGateway',\r\n 'eventBasedGateway',\r\n 'complexGateway',\r\n]);\r\n\r\n/** Distance between layered columns in scene units. Picked so the\r\n * default 100-wide task + 60 gap pattern fits without overlap. */\r\nexport const AUTO_LAYOUT_COL_WIDTH = 160;\r\n\r\n/** Padding from the pool's left edge (after the 30px header strip)\r\n * to the start of the first column. */\r\nexport const AUTO_LAYOUT_X_PAD = 60;\r\n\r\n/** Pool header strip width — the rotated-name column on the left. */\r\nexport const POOL_HEADER_W = 30;\r\n\r\n/** Floor for a lane's height when it grows to fit content - keeps a\r\n * near-empty lane from collapsing to a sliver. */\r\nexport const MIN_LANE_HEIGHT = 80;\r\n\r\nexport type BBox = { x: number; y: number; width: number; height: number };\r\n\r\n/** Live BPMNShape view as returned by `findBpmnShapeElement`. */\r\nexport type LiveShape =\r\n | (BpmnElement & { bpmnElement: string; bounds?: BBox })\r\n | null;\r\n\r\nexport interface AutoLayoutDeps {\r\n findBpmnShapeElement: (doc: BpmnDocument, elementId: string) => LiveShape;\r\n collectLaneIds: (container: BpmnElement) => string[];\r\n /**\r\n * Map each lane id to the set of flow-node ids it semantically owns\r\n * (lane.flowNodeRef). This is the authoritative element-to-lane\r\n * mapping (work item #7566, AC 1) - the layout keys lane membership\r\n * off it rather than off geometry, so a node never silently changes\r\n * lanes. Returns an empty map for a pool with no rendered lanes.\r\n */\r\n collectLaneFlowNodeRefs: (process: BpmnElement) => Map>;\r\n}\r\n\r\n/**\r\n * Result of a layout pass. `nodeBounds` carries the new bounds for every\r\n * flow node we moved; `containerBounds` carries the new bounds for any\r\n * lane or pool shape that had to grow (and restack) so its content fits.\r\n * The caller turns both into commands inside a single composite so one\r\n * undo reverts the whole layout.\r\n */\r\nexport interface AutoLayoutResult {\r\n nodeBounds: Map;\r\n containerBounds: Map;\r\n}\r\n\r\n/**\r\n * Runs the full auto-layout pipeline against a BPMN document and\r\n * returns a map of {nodeId -> new bounds} for every flow node we\r\n * want to move. The caller is responsible for translating this map\r\n * into MoveShapeCommands and pushing them through the command stack.\r\n *\r\n * Returns an empty map when the diagram has nothing to lay out\r\n * (black-box pool, empty processes, no flow nodes).\r\n */\r\nexport function runAutoLayout(\r\n doc: BpmnDocument,\r\n deps: AutoLayoutDeps,\r\n): AutoLayoutResult {\r\n const newBounds = new Map();\r\n const containerBounds = new Map();\r\n const result: AutoLayoutResult = { nodeBounds: newBounds, containerBounds };\r\n\r\n // Walk every collaboration's participants and lay out each pool\r\n // independently. Pool-less diagrams: lay out every root process\r\n // directly (rare - most real diagrams have at least one pool).\r\n let laidOutAtLeastOnePool = false;\r\n for (const root of doc.rootElements) {\r\n if (root.type !== 'collaboration') continue;\r\n const participants = (root as BpmnElement & {\r\n participants?: BpmnElement[];\r\n }).participants;\r\n if (!Array.isArray(participants)) continue;\r\n for (const p of participants) {\r\n if (layoutPoolInto(doc, p, result, deps)) {\r\n laidOutAtLeastOnePool = true;\r\n }\r\n }\r\n }\r\n if (!laidOutAtLeastOnePool) {\r\n for (const root of doc.rootElements) {\r\n if (root.type === 'process') {\r\n layoutProcessInto(doc, root, null, result, deps);\r\n }\r\n }\r\n }\r\n return result;\r\n}\r\n\r\n/**\r\n * Lay out one pool's process. Mutates `newBounds` in place with the\r\n * target bounds for every flow node we want to move. Returns true\r\n * when at least one node was scheduled, false when there was nothing\r\n * to do (black-box pool / empty process / no flow nodes).\r\n */\r\nfunction layoutPoolInto(\r\n doc: BpmnDocument,\r\n participant: BpmnElement,\r\n result: AutoLayoutResult,\r\n deps: AutoLayoutDeps,\r\n): boolean {\r\n const processRef = (participant as BpmnElement & { processRef?: string })\r\n .processRef;\r\n if (!processRef) return false;\r\n const process = doc.byId.get(processRef);\r\n if (!process) return false;\r\n const poolShape = deps.findBpmnShapeElement(doc, participant.id);\r\n if (!poolShape?.bounds) return false;\r\n return layoutProcessInto(doc, process, poolShape, result, deps);\r\n}\r\n\r\n/**\r\n * Lay out one process. Runs the full Sugiyama pipeline:\r\n * Phase 1: cycle breaking (DFS feedback-arc-set)\r\n * Phase 2: longest-path layering\r\n * Phase 3: crossing reduction (median heuristic, 8 sweeps)\r\n * Phase 4: Y-coordinate assignment (simplified Brandes-Köpf with\r\n * trunk centring and lane clamping)\r\n *\r\n * The poolShape is null for pool-less diagrams (rare); lane logic is\r\n * skipped and nodes are placed within the document origin instead.\r\n */\r\nfunction layoutProcessInto(\r\n doc: BpmnDocument,\r\n process: BpmnElement,\r\n poolShape: LiveShape,\r\n result: AutoLayoutResult,\r\n deps: AutoLayoutDeps,\r\n): boolean {\r\n const newBounds = result.nodeBounds;\r\n const flowElements = (process as BpmnElement & {\r\n flowElements?: BpmnElement[];\r\n }).flowElements;\r\n if (!Array.isArray(flowElements) || flowElements.length === 0) return false;\r\n\r\n const flowNodes: BpmnElement[] = [];\r\n const sequenceFlows: BpmnElement[] = [];\r\n for (const fe of flowElements) {\r\n if (fe.type === 'sequenceFlow') sequenceFlows.push(fe);\r\n else if (FLOW_NODE_TYPES_FOR_LAYOUT.has(fe.type)) flowNodes.push(fe);\r\n }\r\n if (flowNodes.length === 0) return false;\r\n\r\n const nodeIds = new Set(flowNodes.map((n) => n.id));\r\n const rawEdges: { source: string; target: string }[] = [];\r\n for (const sf of sequenceFlows) {\r\n const flow = sf as BpmnElement & {\r\n sourceRef?: string;\r\n targetRef?: string;\r\n };\r\n if (!flow.sourceRef || !flow.targetRef) continue;\r\n if (!nodeIds.has(flow.sourceRef) || !nodeIds.has(flow.targetRef)) continue;\r\n rawEdges.push({ source: flow.sourceRef, target: flow.targetRef });\r\n }\r\n\r\n // Phase 1: cycle breaking. Reverses back edges so the layering\r\n // pass below sees a DAG.\r\n const { acyclicEdges } = breakCycles(\r\n flowNodes.map((n) => n.id),\r\n rawEdges,\r\n );\r\n\r\n // Phase 2: longest-path layering.\r\n const layerOf = computeLongestPathLayers(\r\n flowNodes.map((n) => n.id),\r\n acyclicEdges,\r\n );\r\n\r\n // Build layer arrays in document order (initial within-layer order).\r\n const maxLayer = Math.max(0, ...Array.from(layerOf.values()));\r\n const initialLayers: string[][] = [];\r\n for (let i = 0; i <= maxLayer; i += 1) initialLayers.push([]);\r\n for (const node of flowNodes) {\r\n const li = layerOf.get(node.id) ?? 0;\r\n initialLayers[li]?.push(node.id);\r\n }\r\n\r\n // Phase 3: crossing reduction (median heuristic, 8 sweeps).\r\n const orderedLayers = reduceCrossings(initialLayers, acyclicEdges);\r\n\r\n // Map id -> layerOrder so Phase 4 can use the optimised order.\r\n const layerOrder = new Map();\r\n for (const layer of orderedLayers) {\r\n for (let i = 0; i < layer.length; i += 1) {\r\n const id = layer[i];\r\n if (id !== undefined) layerOrder.set(id, i);\r\n }\r\n }\r\n\r\n // Lane bounds in stack order - only meaningful when the process has\r\n // a pool. Sorted top-to-bottom (horizontal pool) so we can restack\r\n // them contiguously after growing.\r\n const lanes: Array<{ id: string; bounds: BBox }> = [];\r\n if (poolShape) {\r\n for (const laneId of deps.collectLaneIds(process)) {\r\n const shape = deps.findBpmnShapeElement(doc, laneId);\r\n if (shape?.bounds) {\r\n lanes.push({\r\n id: laneId,\r\n bounds: { ...shape.bounds },\r\n });\r\n }\r\n }\r\n lanes.sort((a, b) => a.bounds.y - b.bounds.y);\r\n }\r\n\r\n // Element-to-lane mapping. The authoritative source is each lane's\r\n // flowNodeRef (work item #7566, AC 1) so a node never changes lanes.\r\n // Geometry hit-testing is only a fallback for a node that no lane\r\n // claims semantically (e.g. legacy content with empty flowNodeRef).\r\n const laneOfNode = new Map();\r\n if (lanes.length > 0) {\r\n const refs = deps.collectLaneFlowNodeRefs(process);\r\n for (const [laneId, nodeSet] of refs) {\r\n for (const nodeId of nodeSet) {\r\n if (nodeIds.has(nodeId)) laneOfNode.set(nodeId, laneId);\r\n }\r\n }\r\n }\r\n\r\n const xOrigin = poolShape?.bounds\r\n ? poolShape.bounds.x + POOL_HEADER_W + AUTO_LAYOUT_X_PAD\r\n : 80;\r\n\r\n // Build placement input: for each flow node, look up its current\r\n // shape bounds and lane assignment.\r\n const placementNodes: PlacementNode[] = [];\r\n for (const node of flowNodes) {\r\n const shape = deps.findBpmnShapeElement(doc, node.id);\r\n if (!shape?.bounds) continue;\r\n const layer = layerOf.get(node.id) ?? 0;\r\n const order = layerOrder.get(node.id) ?? 0;\r\n\r\n let laneId: string | null = null;\r\n if (lanes.length > 0) {\r\n laneId = laneOfNode.get(node.id) ?? null;\r\n if (laneId === null) {\r\n // Fallback: hit-test the current centre. Used only when the\r\n // node has no flowNodeRef entry on any lane.\r\n const cx = shape.bounds.x + shape.bounds.width / 2;\r\n const cy = shape.bounds.y + shape.bounds.height / 2;\r\n const containingLane = lanes.find(\r\n (l) =>\r\n cx >= l.bounds.x &&\r\n cx <= l.bounds.x + l.bounds.width &&\r\n cy >= l.bounds.y &&\r\n cy <= l.bounds.y + l.bounds.height,\r\n );\r\n laneId = containingLane?.id ?? null;\r\n }\r\n }\r\n\r\n placementNodes.push({\r\n id: node.id,\r\n bounds: {\r\n x: shape.bounds.x,\r\n y: shape.bounds.y,\r\n width: shape.bounds.width,\r\n height: shape.bounds.height,\r\n },\r\n layer,\r\n layerOrder: order,\r\n laneId,\r\n });\r\n }\r\n\r\n // Grow + restack lanes so every lane is tall enough to hold its\r\n // content, then grow the pool to the new total (AC 2 + 3). This\r\n // rewrites `lanes[*].bounds` in place to the resized rectangles, and\r\n // records the pool + lane resizes in result.containerBounds.\r\n if (poolShape?.bounds && lanes.length > 0) {\r\n resizeLanesToFit(\r\n poolShape.bpmnElement,\r\n poolShape.bounds,\r\n lanes,\r\n placementNodes,\r\n result.containerBounds,\r\n );\r\n }\r\n\r\n // Phase 4: Y-coordinate assignment against the (possibly resized)\r\n // lane rectangles. Use the grown pool height so the pool-less branch\r\n // and clamping see the final domain.\r\n const grownPool = poolShape?.bounds\r\n ? result.containerBounds.get(poolShape.bpmnElement) ?? poolShape.bounds\r\n : null;\r\n const placement = placeY({\r\n nodes: placementNodes,\r\n edges: acyclicEdges,\r\n lanes,\r\n poolBounds: grownPool,\r\n xOrigin,\r\n columnWidth: AUTO_LAYOUT_COL_WIDTH,\r\n rowGap: 30,\r\n });\r\n\r\n for (const [id, b] of placement.bounds) newBounds.set(id, b);\r\n\r\n // Phase 5: widen the pool and its lanes to hold what was just placed\r\n // (#7566 AC 2). Height was already grown; width never was, and a\r\n // diagram re-arranged wider than the pool it started in simply drew\r\n // its right-hand end outside the frame. Measured across the layout\r\n // harness's sample library before this: 57 shapes outside their pool\r\n // on 13 of 17 diagrams.\r\n //\r\n // This runs AFTER placement because X is assigned by placeY - at\r\n // resizeLanesToFit time the nodes still carry their OLD x, so widening\r\n // there would size the pool to where the content used to be.\r\n if (poolShape?.bounds && lanes.length > 0) {\r\n widenToFitContent(\r\n poolShape.bpmnElement,\r\n grownPool ?? poolShape.bounds,\r\n lanes,\r\n newBounds,\r\n result.containerBounds,\r\n );\r\n }\r\n\r\n return placementNodes.length > 0;\r\n}\r\n\r\n/**\r\n * Grow the pool and every lane rightwards so the widest placed row fits\r\n * inside them, and record the resizes in `containerBounds`.\r\n *\r\n * Origin-anchored to match `resizeLanesToFit`: x never moves, only\r\n * width grows, and only when the content actually overflows. A diagram\r\n * that already fits is left byte-identical, so this cannot churn the\r\n * geometry of a document nobody widened.\r\n *\r\n * Lanes are widened to the pool's new right edge rather than to their\r\n * own content, because a lane narrower than its pool renders as a short\r\n * band with the pool's border floating beyond it.\r\n */\r\nexport function widenToFitContent(\r\n poolId: string,\r\n poolBounds: BBox,\r\n lanes: Array<{ id: string; bounds: BBox }>,\r\n placed: ReadonlyMap,\r\n containerBounds: Map,\r\n): void {\r\n let contentRight = Number.NEGATIVE_INFINITY;\r\n for (const bounds of placed.values()) {\r\n contentRight = Math.max(contentRight, bounds.x + bounds.width);\r\n }\r\n if (contentRight === Number.NEGATIVE_INFINITY) return;\r\n\r\n // The trailing gap mirrors the leading one, so a widened pool looks\r\n // deliberate rather than trimmed to the last shape.\r\n const requiredRight = contentRight + AUTO_LAYOUT_X_PAD;\r\n const currentRight = poolBounds.x + poolBounds.width;\r\n if (requiredRight <= currentRight) return;\r\n\r\n const newPoolBounds: BBox = {\r\n x: poolBounds.x,\r\n y: poolBounds.y,\r\n width: requiredRight - poolBounds.x,\r\n height: poolBounds.height,\r\n };\r\n containerBounds.set(poolId, newPoolBounds);\r\n\r\n for (const lane of lanes) {\r\n const laneWidth = requiredRight - lane.bounds.x;\r\n if (laneWidth <= lane.bounds.width) continue;\r\n const current = containerBounds.get(lane.id) ?? lane.bounds;\r\n const newLaneBounds: BBox = {\r\n x: current.x,\r\n y: current.y,\r\n width: laneWidth,\r\n height: current.height,\r\n };\r\n containerBounds.set(lane.id, newLaneBounds);\r\n lane.bounds = newLaneBounds;\r\n }\r\n}\r\n\r\n/**\r\n * Grow each lane so it can hold the nodes assigned to it without\r\n * overlap, restack the lanes contiguously from the pool's top edge,\r\n * and grow the pool to the new total height. Mutates `lanes[*].bounds`\r\n * to the resized rectangles (so the placement pass below uses them) and\r\n * records every changed lane + the pool in `containerBounds`.\r\n *\r\n * Horizontal pools only stack lanes vertically, which is what the\r\n * editor produces; the lane x/width are inherited from the existing\r\n * lane shapes and left unchanged. Lanes keep their original top-to-\r\n * bottom order - only their heights and y offsets move.\r\n */\r\nfunction resizeLanesToFit(\r\n poolId: string,\r\n poolBounds: BBox,\r\n lanes: Array<{ id: string; bounds: BBox }>,\r\n placementNodes: readonly PlacementNode[],\r\n containerBounds: Map,\r\n): void {\r\n const required = computeRequiredLaneHeights(\r\n placementNodes,\r\n lanes.map((l) => l.id),\r\n 30,\r\n MIN_LANE_HEIGHT,\r\n );\r\n\r\n // Restack from the first lane's current top edge so the pool's origin\r\n // does not move (the pool only grows downward, matching resizePool's\r\n // origin-anchored contract).\r\n let cursorY = lanes[0]?.bounds.y ?? poolBounds.y;\r\n let totalLaneHeight = 0;\r\n for (const lane of lanes) {\r\n const newHeight = Math.max(\r\n required.get(lane.id) ?? MIN_LANE_HEIGHT,\r\n lane.bounds.height,\r\n );\r\n const newBounds: BBox = {\r\n x: lane.bounds.x,\r\n y: cursorY,\r\n width: lane.bounds.width,\r\n height: newHeight,\r\n };\r\n // Record the resize only when something actually changed.\r\n if (\r\n newBounds.y !== lane.bounds.y ||\r\n newBounds.height !== lane.bounds.height\r\n ) {\r\n containerBounds.set(lane.id, newBounds);\r\n }\r\n // Mutate the working copy so placement uses the grown rectangle.\r\n lane.bounds = newBounds;\r\n cursorY += newHeight;\r\n totalLaneHeight += newHeight;\r\n }\r\n\r\n // Grow the pool to wrap the stacked lanes. Pool height tracks the sum\r\n // of lane heights; width/origin are left as-is.\r\n const newPoolHeight = Math.max(poolBounds.height, totalLaneHeight);\r\n if (newPoolHeight !== poolBounds.height) {\r\n containerBounds.set(poolId, {\r\n x: poolBounds.x,\r\n y: poolBounds.y,\r\n width: poolBounds.width,\r\n height: newPoolHeight,\r\n });\r\n }\r\n}\r\n\r\n/**\r\n * Longest-path layering on a directed graph. Each edge `(u, v)`\r\n * forces `layer[v] >= layer[u] + 1`. Iterated to fixed point;\r\n * cycles are bounded by the node count so we don't loop forever\r\n * on a malformed flow graph.\r\n */\r\nexport function computeLongestPathLayers(\r\n nodes: readonly string[],\r\n edges: ReadonlyArray<{ source: string; target: string }>,\r\n): Map {\r\n const layer = new Map();\r\n for (const id of nodes) layer.set(id, 0);\r\n let changed = true;\r\n let iterations = 0;\r\n const maxIterations = nodes.length + 1;\r\n while (changed && iterations < maxIterations) {\r\n changed = false;\r\n iterations += 1;\r\n for (const e of edges) {\r\n const src = layer.get(e.source);\r\n const tgt = layer.get(e.target);\r\n if (src === undefined || tgt === undefined) continue;\r\n if (tgt < src + 1) {\r\n layer.set(e.target, src + 1);\r\n changed = true;\r\n }\r\n }\r\n }\r\n return layer;\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - en message catalog\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: the en translation catalog for the editor chrome.\r\n * GENERATED FILE - do not edit by hand; regenerate via the i18n catalog\r\n * build step. English is the complete source of truth for the key set;\r\n * element-type and toolbar/property terms are reused from the Blazor\r\n * BpmStudioResources .resx so the editor matches the surrounding UI.\r\n * Other strings are machine-authored and pending native review. Keys\r\n * absent from a non-English catalog fall back to English.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { MessageCatalog } from '../types.js';\r\n\r\nexport const en: MessageCatalog = {\r\n 'type.startEvent': 'Start event',\r\n 'type.endEvent': 'End event',\r\n 'type.intermediateThrowEvent': 'Intermediate throw event',\r\n 'type.intermediateCatchEvent': 'Intermediate catch event',\r\n 'type.boundaryEvent': 'Boundary event',\r\n 'type.task': 'Task',\r\n 'type.userTask': 'User task',\r\n 'type.serviceTask': 'Service task',\r\n 'type.sendTask': 'Send task',\r\n 'type.receiveTask': 'Receive task',\r\n 'type.manualTask': 'Manual task',\r\n 'type.businessRuleTask': 'Business rule task',\r\n 'type.scriptTask': 'Script task',\r\n 'type.callActivity': 'Call activity',\r\n 'type.exclusiveGateway': 'Exclusive gateway',\r\n 'type.parallelGateway': 'Parallel gateway',\r\n 'type.inclusiveGateway': 'Inclusive gateway',\r\n 'type.eventBasedGateway': 'Event-based gateway',\r\n 'type.complexGateway': 'Complex gateway',\r\n 'type.subProcess': 'Sub-process',\r\n 'type.adHocSubProcess': 'Ad-hoc sub-process',\r\n 'type.transaction': 'Transaction',\r\n 'type.dataObject': 'Data object',\r\n 'type.dataObjectReference': 'Data object reference',\r\n 'type.dataObject.description': 'The data itself, defined once in the process.',\r\n 'type.dataObjectReference.description':\r\n 'Points at an existing data object. BPMN draws both with the same page symbol.',\r\n 'type.dataStoreReference': 'Data store',\r\n 'type.textAnnotation': 'Text annotation',\r\n 'type.group': 'Group',\r\n 'type.participant': 'Pool',\r\n 'type.lane': 'Lane',\r\n 'type.sequenceFlow': 'Sequence flow',\r\n 'type.messageFlow': 'Message flow',\r\n 'type.association': 'Association',\r\n 'palette.group.start': 'Start event',\r\n 'palette.group.intermediate': 'Intermediate event',\r\n 'palette.group.end': 'End event',\r\n 'palette.group.task': 'Task',\r\n 'palette.group.gateway': 'Gateway',\r\n 'palette.group.subprocess': 'Sub-process',\r\n 'palette.group.data': 'Data object',\r\n 'palette.group.annotation': 'Text annotation',\r\n 'palette.structural.pool': 'Pool',\r\n 'palette.structural.lane': 'Lane',\r\n 'palette.section.structure': 'Structure',\r\n 'palette.category.events': 'Events',\r\n 'palette.category.tasks': 'Tasks',\r\n 'palette.category.gateways': 'Gateways',\r\n 'palette.category.containers': 'Containers',\r\n 'palette.aria.toolbar': 'BPMN shape palette',\r\n 'palette.flyout.variantsSuffix': '{label} variants',\r\n 'palette.toggle.title': 'Toggle wide palette',\r\n 'toolbar.aria.controls': 'BPMN editor controls',\r\n 'toolbar.undo': 'Undo',\r\n 'toolbar.redo': 'Redo',\r\n 'toolbar.zoomOut': 'Zoom out',\r\n 'toolbar.fit': 'Fit to view',\r\n 'toolbar.zoomIn': 'Zoom in',\r\n 'toolbar.routeCleanup': 'Clean up routes',\r\n 'toolbar.validation': 'Show validation report',\r\n 'toolbar.documentation': 'Show process documentation preview',\r\n 'toolbar.templates': 'Open quick pattern gallery',\r\n 'contextPad.addLaneBelow': 'Add lane below',\r\n 'contextPad.addLaneAbove': 'Add lane above',\r\n 'contextPad.distributeLanes': 'Distribute lanes evenly',\r\n 'contextPad.deletePool': 'Delete pool',\r\n 'contextPad.deleteLane': 'Delete lane',\r\n 'contextPad.delete': 'Delete',\r\n 'contextPad.appendElement': 'Append element...',\r\n 'contextPad.appendTaskAndConnect': 'Append task and connect',\r\n 'contextPad.attachBoundary': 'Attach boundary event...',\r\n 'boundaryKind.message': 'Message',\r\n 'boundaryKind.timer': 'Timer',\r\n 'boundaryKind.error': 'Error',\r\n 'boundaryKind.escalation': 'Escalation',\r\n 'boundaryKind.signal': 'Signal',\r\n 'boundaryKind.conditional': 'Conditional',\r\n 'boundaryKind.compensation': 'Compensation',\r\n 'propertyPanel.aria.panel': 'Selected element details',\r\n 'propertyPanel.tierBadge': 'Tier {tier}',\r\n 'propertyPanel.eventLabel': 'Event',\r\n 'propertyPanel.loopLabel': 'Loop',\r\n 'propertyPanel.namePlaceholder': '(unnamed)',\r\n 'propertyPanel.conditionLabel': 'Condition',\r\n 'propertyPanel.conditionPlaceholder': 'e.g. ${amount > 100}',\r\n 'propertyPanel.defaultFlowFrom': 'Default flow from {source}',\r\n 'propertyPanel.itemCountOne': '{count} item',\r\n 'propertyPanel.itemCountMany': '{count} items',\r\n 'appendMenu.aria.default': 'Append element',\r\n 'appendMenu.search.aria': 'Filter element types',\r\n 'appendMenu.search.placeholderFiltered': 'Filter...',\r\n 'appendMenu.search.placeholder': 'Search element type...',\r\n 'appendMenu.empty': 'No matches.',\r\n 'appendMenu.section.tasks': 'Tasks',\r\n 'appendMenu.section.gateways': 'Gateways',\r\n 'appendMenu.section.events': 'Events',\r\n 'appendMenu.section.containers': 'Containers',\r\n 'appendMenu.section.data': 'Data',\r\n 'appendMenu.section.other': 'Other',\r\n 'quickPattern.aria.menu': 'Quick BPMN patterns',\r\n 'quickPattern.title': 'Quick patterns',\r\n 'quickPattern.approval.label': 'Approval flow',\r\n 'quickPattern.approval.desc': 'User task followed by a decision point.',\r\n 'quickPattern.decision.label': 'Decision branch',\r\n 'quickPattern.decision.desc': 'Exclusive gateway with two outgoing task branches.',\r\n 'quickPattern.parallelSplitJoin.label': 'Parallel split/join',\r\n 'quickPattern.parallelSplitJoin.desc': 'Parallel gateway, two branches, and a join gateway.',\r\n 'quickPattern.exceptionPath.label': 'Exception path',\r\n 'quickPattern.exceptionPath.desc': 'Task followed by an exception event path.',\r\n 'quickPattern.timerEscalation.label': 'Timer escalation',\r\n 'quickPattern.timerEscalation.desc': 'Task followed by an intermediate timer event.',\r\n 'reportPanel.close.aria': 'Close report',\r\n 'minimap.aria': 'Diagram overview minimap',\r\n 'warnings.partialViewOne': 'Partial view: {count} drill-down view not rendered',\r\n 'warnings.partialViewMany': 'Partial view: {count} drill-down views not rendered',\r\n 'warnings.unplacedOne': '{count} element without position data - not shown',\r\n 'warnings.unplacedMany': '{count} elements without position data - not shown',\r\n 'warnings.unsupportedTypes': 'Unsupported element types shown as placeholders: {types}{more}',\r\n 'warnings.moreSuffix': ' (+{count} more)',\r\n 'warnings.titleOne': '1 notice',\r\n 'warnings.titleMany': '{count} notices',\r\n 'severity.error': 'Error',\r\n 'severity.warning': 'Warning',\r\n 'severity.info': 'Info',\r\n 'lint.containerWord.process': 'Process',\r\n 'lint.containerWord.subProcess': 'Sub-process',\r\n 'lint.direction.diverging': 'Diverging',\r\n 'lint.direction.converging': 'Converging',\r\n 'lint.direction.mixed': 'Mixed',\r\n 'lint.startEventRequired': '{container} \\'{label}\\' has no start event.',\r\n 'lint.endEventRequired': '{container} \\'{label}\\' has no end event.',\r\n 'lint.singleBlankStartEvent': 'Process \\'{label}\\' has multiple blank start events; type each entry point or merge them.',\r\n 'lint.noDisconnected': '{type} \\'{label}\\' has no incoming or outgoing flows.',\r\n 'lint.deadEnd': '{type} \\'{label}\\' is a dead end (no outgoing sequence flow).',\r\n 'lint.unreachable': '{type} \\'{label}\\' is unreachable (no incoming sequence flow).',\r\n 'lint.danglingFlow.source': 'Sequence flow \\'{flow}\\' has unresolved source \\'{ref}\\'.',\r\n 'lint.danglingFlow.target': 'Sequence flow \\'{flow}\\' has unresolved target \\'{ref}\\'.',\r\n 'lint.danglingFlow.empty': '(empty)',\r\n 'lint.duplicateId': 'Element id \\'{id}\\' appears {count} times in the document.',\r\n 'lint.noImplicitStart': '{type} \\'{label}\\' acts as an implicit start; add an explicit start event upstream.',\r\n 'lint.noImplicitEnd': '{type} \\'{label}\\' acts as an implicit end; add an explicit end event downstream.',\r\n 'lint.noImplicitSplit': '{type} \\'{label}\\' has {count} outgoing flows (implicit split); use an explicit gateway.',\r\n 'lint.fakeJoin': '{type} \\'{label}\\' has {count} incoming flows (fake join); use an explicit joining gateway.',\r\n 'lint.noGatewayJoinFork': '{type} \\'{label}\\' both joins ({incoming} in) and forks ({outgoing} out); split it into two gateways.',\r\n 'lint.noDuplicateSequenceFlows': 'Sequence flow \\'{flow}\\' duplicates flow \\'{first}\\' (same source and target).',\r\n 'lint.superfluousGateway': '{type} \\'{label}\\' has one in and one out - it neither joins nor forks and can be removed.',\r\n 'lint.gatewayMismatch.diverging': '{type} \\'{label}\\' is declared {direction} but has {count} outgoing flow(s).',\r\n 'lint.gatewayMismatch.converging': '{type} \\'{label}\\' is declared {direction} but has {count} incoming flow(s).',\r\n 'lint.gatewayMismatch.mixed': '{type} \\'{label}\\' is declared {direction} but has {incoming} incoming / {outgoing} outgoing flow(s).',\r\n 'lint.eventBasedGateway': 'Event-based gateway \\'{label}\\' has a successor of type {type}; only intermediate catch events and receive tasks are allowed.',\r\n 'lint.subProcessBlankStartEvent': 'Start event \\'{label}\\' inside sub-process \\'{container}\\' is typed; embedded sub-processes must use blank start events.',\r\n 'lint.eventSubProcessTypedStartEvent': 'Start event \\'{label}\\' in event sub-process \\'{container}\\' is blank; event sub-processes require a typed start.',\r\n 'lint.conditionalFlows': 'Outgoing flow \\'{flow}\\' from {label} needs a condition (sibling flows are conditional).',\r\n 'lint.labelRequired': '{type} \\'{id}\\' has no name.',\r\n 'lint.noComplexGateway': 'Complex gateway \\'{label}\\' is hard to reason about; prefer exclusive, parallel, or inclusive gateways.',\r\n 'lint.noInclusiveGateway': 'Inclusive gateway \\'{label}\\' (OR semantics) is harder to reason about; consider exclusive or parallel.',\r\n 'lint.superfluousTermination': 'Terminate end event \\'{label}\\' is redundant - one terminate end cancels all parallel activity.',\r\n 'lint.verbObjectActivityLabel': 'Activity \\'{label}\\' should start with an action verb (e.g. \"Approve invoice\", \"Send notification\").',\r\n 'lint.pastParticipleEventLabel': 'Event \\'{label}\\' should be labelled as a completed state (e.g. \"Invoice received\", \"Order shipped\").',\r\n 'lint.decomposeLargeProcess': 'Process \\'{label}\\' has {count} flow nodes (threshold {threshold}); consider decomposing into sub-processes.',\r\n 'lint.minimizeRoutingPaths.outgoing': '{type} \\'{label}\\' has {count} outgoing paths (threshold {threshold}); consider decomposing the branching.',\r\n 'lint.minimizeRoutingPaths.incoming': '{type} \\'{label}\\' has {count} incoming paths (threshold {threshold}); consider merging upstream.',\r\n 'lint.quickFix.removeGateway': 'Remove gateway',\r\n 'lint.quickFix.deleteElement': 'Delete element',\r\n 'lint.quickFix.deleteDuplicateFlow': 'Delete duplicate flow',\r\n 'replaceMenu.title': 'Change type to...',\r\n 'report.documentation.title': 'Process documentation preview',\r\n 'report.routeCleanup.title': 'Route cleanup complete',\r\n 'report.validation.title': 'BPMN validation report',\r\n 'report.validation.count': 'Validation issues: {count}',\r\n 'report.validation.none': 'No BPMN validation issues found.',\r\n};\r\n","/**\r\n * mindzie.BpmnEditor - de message catalog\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: the de translation catalog for the editor chrome.\r\n * GENERATED FILE - do not edit by hand; regenerate via the i18n catalog\r\n * build step. English is the complete source of truth for the key set;\r\n * element-type and toolbar/property terms are reused from the Blazor\r\n * BpmStudioResources .resx so the editor matches the surrounding UI.\r\n * Other strings are machine-authored and pending native review. Keys\r\n * absent from a non-English catalog fall back to English.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { MessageCatalog } from '../types.js';\r\n\r\nexport const de: MessageCatalog = {\r\n 'type.startEvent': 'Startereignis',\r\n 'type.endEvent': 'Endereignis',\r\n 'type.intermediateThrowEvent': 'Zwischenereignis (auslösend)',\r\n 'type.intermediateCatchEvent': 'Zwischenereignis (empfangend)',\r\n 'type.boundaryEvent': 'Begrenzungsereignis',\r\n 'type.task': 'Aufgabe',\r\n 'type.userTask': 'Benutzeraufgabe',\r\n 'type.serviceTask': 'Serviceaufgabe',\r\n 'type.sendTask': 'Sendeaufgabe',\r\n 'type.receiveTask': 'Empfangsaufgabe',\r\n 'type.manualTask': 'Manuelle Aufgabe',\r\n 'type.businessRuleTask': 'Geschäftsregelaufgabe',\r\n 'type.scriptTask': 'Skriptaufgabe',\r\n 'type.callActivity': 'Aufrufaktivität',\r\n 'type.exclusiveGateway': 'Exklusives Gateway',\r\n 'type.parallelGateway': 'Paralleles Gateway',\r\n 'type.inclusiveGateway': 'Inklusives Gateway',\r\n 'type.eventBasedGateway': 'Ereignisbasiertes Gateway',\r\n 'type.complexGateway': 'Komplexes Gateway',\r\n 'type.subProcess': 'Teilprozess',\r\n 'type.adHocSubProcess': 'Teilprozess',\r\n 'type.transaction': 'Teilprozess',\r\n 'type.dataObject': 'Datenobjekt',\r\n 'type.dataObjectReference': 'Datenobjektreferenz',\r\n 'type.dataObject.description': 'Die Daten selbst, einmal im Prozess definiert.',\r\n 'type.dataObjectReference.description':\r\n 'Verweist auf ein vorhandenes Datenobjekt. BPMN zeichnet beide mit demselben Seitensymbol.',\r\n 'type.dataStoreReference': 'Datenspeicherreferenz',\r\n 'type.textAnnotation': 'Textanmerkung',\r\n 'type.group': 'Gruppe',\r\n 'type.participant': 'Pool',\r\n 'type.lane': 'Bahn',\r\n 'type.sequenceFlow': 'Sequenzfluss',\r\n 'type.messageFlow': 'Nachrichtenfluss',\r\n 'type.association': 'Assoziation',\r\n 'palette.group.start': 'Startereignis',\r\n 'palette.group.intermediate': 'Zwischenereignis',\r\n 'palette.group.end': 'Endereignis',\r\n 'palette.group.task': 'Aufgabe',\r\n 'palette.group.gateway': 'Gateway',\r\n 'palette.group.subprocess': 'Teilprozess',\r\n 'palette.group.data': 'Datenobjekt',\r\n 'palette.group.annotation': 'Textanmerkung',\r\n 'palette.structural.pool': 'Pool',\r\n 'palette.structural.lane': 'Bahn',\r\n 'palette.section.structure': 'Struktur',\r\n 'palette.category.events': 'Ereignisse',\r\n 'palette.category.tasks': 'Aufgaben',\r\n 'palette.category.gateways': 'Gateways',\r\n 'palette.category.containers': 'Container',\r\n 'palette.aria.toolbar': 'BPMN-Formpalette',\r\n 'palette.flyout.variantsSuffix': '{label}-Varianten',\r\n 'palette.toggle.title': 'Breite Palette umschalten',\r\n 'toolbar.aria.controls': 'BPMN-Editor-Steuerelemente',\r\n 'toolbar.undo': 'Rückgängig',\r\n 'toolbar.redo': 'Wiederholen',\r\n 'toolbar.zoomOut': 'Verkleinern',\r\n 'toolbar.fit': 'Anpassen',\r\n 'toolbar.zoomIn': 'Vergrößern',\r\n 'toolbar.routeCleanup': 'Routen bereinigen',\r\n 'toolbar.validation': 'Validierungsbericht anzeigen',\r\n 'toolbar.documentation': 'Vorschau der Prozessdokumentation anzeigen',\r\n 'toolbar.templates': 'Galerie der Schnellmuster öffnen',\r\n 'contextPad.addLaneBelow': 'Bahn unten hinzufügen',\r\n 'contextPad.addLaneAbove': 'Bahn oben hinzufügen',\r\n 'contextPad.distributeLanes': 'Bahnen gleichmäßig verteilen',\r\n 'contextPad.deletePool': 'Pool löschen',\r\n 'contextPad.deleteLane': 'Bahn löschen',\r\n 'contextPad.delete': 'Löschen',\r\n 'contextPad.appendElement': 'Element anhängen ...',\r\n 'contextPad.appendTaskAndConnect': 'Aufgabe anhängen und verbinden',\r\n 'contextPad.attachBoundary': 'Begrenzungsereignis anhängen...',\r\n 'boundaryKind.message': 'Nachricht',\r\n 'boundaryKind.timer': 'Timer',\r\n 'boundaryKind.error': 'Fehler',\r\n 'boundaryKind.escalation': 'Eskalation',\r\n 'boundaryKind.signal': 'Signal',\r\n 'boundaryKind.conditional': 'Bedingt',\r\n 'boundaryKind.compensation': 'Kompensation',\r\n 'propertyPanel.aria.panel': 'Details des ausgewählten Elements',\r\n 'propertyPanel.tierBadge': 'Ebene {tier}',\r\n 'propertyPanel.eventLabel': 'Ereignis',\r\n 'propertyPanel.loopLabel': 'Schleife',\r\n 'propertyPanel.namePlaceholder': '(unbenannt)',\r\n 'propertyPanel.conditionLabel': 'Bedingung',\r\n 'propertyPanel.conditionPlaceholder': 'z. B. ${amount > 100}',\r\n 'propertyPanel.defaultFlowFrom': 'Standardfluss von {source}',\r\n 'propertyPanel.itemCountOne': '{count} Element',\r\n 'propertyPanel.itemCountMany': '{count} Elemente',\r\n 'appendMenu.aria.default': 'Element anhängen',\r\n 'appendMenu.search.aria': 'Elementtypen filtern',\r\n 'appendMenu.search.placeholderFiltered': 'Filtern ...',\r\n 'appendMenu.search.placeholder': 'Elementtyp suchen ...',\r\n 'appendMenu.empty': 'Keine Treffer.',\r\n 'appendMenu.section.tasks': 'Aufgaben',\r\n 'appendMenu.section.gateways': 'Gateways',\r\n 'appendMenu.section.events': 'Ereignisse',\r\n 'appendMenu.section.containers': 'Container',\r\n 'appendMenu.section.data': 'Daten',\r\n 'appendMenu.section.other': 'Sonstige',\r\n 'quickPattern.aria.menu': 'BPMN-Schnellmuster',\r\n 'quickPattern.title': 'Schnellmuster',\r\n 'quickPattern.approval.label': 'Genehmigungsablauf',\r\n 'quickPattern.approval.desc': 'Benutzeraufgabe gefolgt von einem Entscheidungspunkt.',\r\n 'quickPattern.decision.label': 'Entscheidungsverzweigung',\r\n 'quickPattern.decision.desc': 'Exklusives Gateway mit zwei ausgehenden Aufgabenzweigen.',\r\n 'quickPattern.parallelSplitJoin.label': 'Paralleles Aufteilen/Zusammenführen',\r\n 'quickPattern.parallelSplitJoin.desc': 'Paralleles Gateway, zwei Zweige und ein zusammenführendes Gateway.',\r\n 'quickPattern.exceptionPath.label': 'Ausnahmepfad',\r\n 'quickPattern.exceptionPath.desc': 'Aufgabe gefolgt von einem Ausnahmeereignispfad.',\r\n 'quickPattern.timerEscalation.label': 'Timer-Eskalation',\r\n 'quickPattern.timerEscalation.desc': 'Aufgabe gefolgt von einem Zwischen-Timer-Ereignis.',\r\n 'reportPanel.close.aria': 'Bericht schließen',\r\n 'minimap.aria': 'Übersichts-Minimap des Diagramms',\r\n 'warnings.partialViewOne': 'Teilansicht: {count} Detailansicht nicht gerendert',\r\n 'warnings.partialViewMany': 'Teilansicht: {count} Detailansichten nicht gerendert',\r\n 'warnings.unplacedOne': '{count} Element ohne Positionsdaten – nicht angezeigt',\r\n 'warnings.unplacedMany': '{count} Elemente ohne Positionsdaten – nicht angezeigt',\r\n 'warnings.unsupportedTypes': 'Nicht unterstützte Elementtypen werden als Platzhalter angezeigt: {types}{more}',\r\n 'warnings.moreSuffix': ' (+{count} weitere)',\r\n 'warnings.titleOne': '1 Hinweis',\r\n 'warnings.titleMany': '{count} Hinweise',\r\n 'severity.error': 'Fehler',\r\n 'severity.warning': 'Warnung',\r\n 'severity.info': 'Info',\r\n 'lint.containerWord.process': 'Prozess',\r\n 'lint.containerWord.subProcess': 'Teilprozess',\r\n 'lint.direction.diverging': 'Divergierend',\r\n 'lint.direction.converging': 'Konvergierend',\r\n 'lint.direction.mixed': 'Gemischt',\r\n 'lint.startEventRequired': '{container} \\'{label}\\' hat kein Startereignis.',\r\n 'lint.endEventRequired': '{container} \\'{label}\\' hat kein Endereignis.',\r\n 'lint.singleBlankStartEvent': 'Prozess \\'{label}\\' hat mehrere leere Startereignisse; typisieren Sie jeden Einstiegspunkt oder führen Sie sie zusammen.',\r\n 'lint.noDisconnected': '{type} \\'{label}\\' hat keine eingehenden oder ausgehenden Flüsse.',\r\n 'lint.deadEnd': '{type} \\'{label}\\' ist eine Sackgasse (kein ausgehender Sequenzfluss).',\r\n 'lint.unreachable': '{type} \\'{label}\\' ist nicht erreichbar (kein eingehender Sequenzfluss).',\r\n 'lint.danglingFlow.source': 'Sequenzfluss \\'{flow}\\' hat eine nicht aufgelöste Quelle \\'{ref}\\'.',\r\n 'lint.danglingFlow.target': 'Sequenzfluss \\'{flow}\\' hat ein nicht aufgelöstes Ziel \\'{ref}\\'.',\r\n 'lint.danglingFlow.empty': '(leer)',\r\n 'lint.duplicateId': 'Element-ID \\'{id}\\' erscheint {count}-mal im Dokument.',\r\n 'lint.noImplicitStart': '{type} \\'{label}\\' fungiert als impliziter Start; fügen Sie stromaufwärts ein explizites Startereignis hinzu.',\r\n 'lint.noImplicitEnd': '{type} \\'{label}\\' fungiert als implizites Ende; fügen Sie stromabwärts ein explizites Endereignis hinzu.',\r\n 'lint.noImplicitSplit': '{type} \\'{label}\\' hat {count} ausgehende Flüsse (implizite Aufteilung); verwenden Sie ein explizites Gateway.',\r\n 'lint.fakeJoin': '{type} \\'{label}\\' hat {count} eingehende Flüsse (unechte Zusammenführung); verwenden Sie ein explizites zusammenführendes Gateway.',\r\n 'lint.noGatewayJoinFork': '{type} \\'{label}\\' führt sowohl zusammen ({incoming} eingehend) als auch verzweigt ({outgoing} ausgehend); teilen Sie es in zwei Gateways auf.',\r\n 'lint.noDuplicateSequenceFlows': 'Sequenzfluss \\'{flow}\\' dupliziert Fluss \\'{first}\\' (gleiche Quelle und gleiches Ziel).',\r\n 'lint.superfluousGateway': '{type} \\'{label}\\' hat einen eingehenden und einen ausgehenden Fluss – es führt weder zusammen noch verzweigt es und kann entfernt werden.',\r\n 'lint.gatewayMismatch.diverging': '{type} \\'{label}\\' ist als {direction} deklariert, hat aber {count} ausgehende(n) Fluss/Flüsse.',\r\n 'lint.gatewayMismatch.converging': '{type} \\'{label}\\' ist als {direction} deklariert, hat aber {count} eingehende(n) Fluss/Flüsse.',\r\n 'lint.gatewayMismatch.mixed': '{type} \\'{label}\\' ist als {direction} deklariert, hat aber {incoming} eingehende / {outgoing} ausgehende Flüsse.',\r\n 'lint.eventBasedGateway': 'Ereignisbasiertes Gateway \\'{label}\\' hat einen Nachfolger vom Typ {type}; nur Zwischenereignisse (empfangend) und Empfangsaufgaben sind zulässig.',\r\n 'lint.subProcessBlankStartEvent': 'Startereignis \\'{label}\\' im Teilprozess \\'{container}\\' ist typisiert; eingebettete Teilprozesse müssen leere Startereignisse verwenden.',\r\n 'lint.eventSubProcessTypedStartEvent': 'Startereignis \\'{label}\\' im Ereignis-Teilprozess \\'{container}\\' ist leer; Ereignis-Teilprozesse erfordern einen typisierten Start.',\r\n 'lint.conditionalFlows': 'Ausgehender Fluss \\'{flow}\\' von {label} benötigt eine Bedingung (Geschwisterflüsse sind bedingt).',\r\n 'lint.labelRequired': '{type} \\'{id}\\' hat keinen Namen.',\r\n 'lint.noComplexGateway': 'Komplexes Gateway \\'{label}\\' ist schwer nachvollziehbar; bevorzugen Sie exklusive, parallele oder inklusive Gateways.',\r\n 'lint.noInclusiveGateway': 'Inklusives Gateway \\'{label}\\' (ODER-Semantik) ist schwerer nachvollziehbar; erwägen Sie exklusiv oder parallel.',\r\n 'lint.superfluousTermination': 'Abbruch-Endereignis \\'{label}\\' ist redundant – ein Abbruch-Ende beendet alle parallelen Aktivitäten.',\r\n 'lint.verbObjectActivityLabel': 'Aktivität \\'{label}\\' sollte mit einem Handlungsverb beginnen (z. B. „Rechnung genehmigen“, „Benachrichtigung senden“).',\r\n 'lint.pastParticipleEventLabel': 'Ereignis \\'{label}\\' sollte als abgeschlossener Zustand benannt werden (z. B. „Rechnung erhalten“, „Bestellung versandt“).',\r\n 'lint.decomposeLargeProcess': 'Prozess \\'{label}\\' hat {count} Flussknoten (Schwellenwert {threshold}); erwägen Sie eine Zerlegung in Teilprozesse.',\r\n 'lint.minimizeRoutingPaths.outgoing': '{type} \\'{label}\\' hat {count} ausgehende Pfade (Schwellenwert {threshold}); erwägen Sie eine Zerlegung der Verzweigung.',\r\n 'lint.minimizeRoutingPaths.incoming': '{type} \\'{label}\\' hat {count} eingehende Pfade (Schwellenwert {threshold}); erwägen Sie eine Zusammenführung stromaufwärts.',\r\n 'lint.quickFix.removeGateway': 'Gateway entfernen',\r\n 'lint.quickFix.deleteElement': 'Element löschen',\r\n 'lint.quickFix.deleteDuplicateFlow': 'Doppelten Fluss löschen',\r\n 'replaceMenu.title': 'Typ ändern in ...',\r\n 'report.documentation.title': 'Vorschau der Prozessdokumentation',\r\n 'report.routeCleanup.title': 'Routen bereinigen',\r\n 'report.validation.title': 'BPMN-Validierungsbericht',\r\n 'report.validation.count': 'Validierungsprobleme: {count}',\r\n 'report.validation.none': 'Keine BPMN-Validierungsprobleme gefunden.',\r\n};\r\n","/**\r\n * mindzie.BpmnEditor - fr message catalog\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: the fr translation catalog for the editor chrome.\r\n * GENERATED FILE - do not edit by hand; regenerate via the i18n catalog\r\n * build step. English is the complete source of truth for the key set;\r\n * element-type and toolbar/property terms are reused from the Blazor\r\n * BpmStudioResources .resx so the editor matches the surrounding UI.\r\n * Other strings are machine-authored and pending native review. Keys\r\n * absent from a non-English catalog fall back to English.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { MessageCatalog } from '../types.js';\r\n\r\nexport const fr: MessageCatalog = {\r\n 'type.startEvent': 'Événement de début',\r\n 'type.endEvent': 'Événement de fin',\r\n 'type.intermediateThrowEvent': 'Événement intermédiaire d\\'émission',\r\n 'type.intermediateCatchEvent': 'Événement intermédiaire de réception',\r\n 'type.boundaryEvent': 'Événement de bordure',\r\n 'type.task': 'Tâche',\r\n 'type.userTask': 'Tâche utilisateur',\r\n 'type.serviceTask': 'Tâche de service',\r\n 'type.sendTask': 'Tâche d\\'envoi',\r\n 'type.receiveTask': 'Tâche de réception',\r\n 'type.manualTask': 'Tâche manuelle',\r\n 'type.businessRuleTask': 'Tâche de règle métier',\r\n 'type.scriptTask': 'Tâche de script',\r\n 'type.callActivity': 'Activité d\\'appel',\r\n 'type.exclusiveGateway': 'Passerelle exclusive',\r\n 'type.parallelGateway': 'Passerelle parallèle',\r\n 'type.inclusiveGateway': 'Passerelle inclusive',\r\n 'type.eventBasedGateway': 'Passerelle basée sur les événements',\r\n 'type.complexGateway': 'Passerelle complexe',\r\n 'type.subProcess': 'Sous-processus',\r\n 'type.adHocSubProcess': 'Sous-processus',\r\n 'type.transaction': 'Sous-processus',\r\n 'type.dataObject': 'Objet de données',\r\n 'type.dataObjectReference': 'Référence d\\'objet de données',\r\n 'type.dataObject.description':\r\n 'Les données elles-mêmes, définies une seule fois dans le processus.',\r\n 'type.dataObjectReference.description':\r\n 'Pointe vers un objet de données existant. BPMN dessine les deux avec le même symbole de page.',\r\n 'type.dataStoreReference': 'Référence de magasin de données',\r\n 'type.textAnnotation': 'Annotation textuelle',\r\n 'type.group': 'Groupe',\r\n 'type.participant': 'Pool',\r\n 'type.lane': 'Couloir',\r\n 'type.sequenceFlow': 'Flux de séquence',\r\n 'type.messageFlow': 'Flux de message',\r\n 'type.association': 'Association',\r\n 'palette.group.start': 'Événement de début',\r\n 'palette.group.intermediate': 'Événement intermédiaire',\r\n 'palette.group.end': 'Événement de fin',\r\n 'palette.group.task': 'Tâche',\r\n 'palette.group.gateway': 'Passerelle',\r\n 'palette.group.subprocess': 'Sous-processus',\r\n 'palette.group.data': 'Objet de données',\r\n 'palette.group.annotation': 'Annotation textuelle',\r\n 'palette.structural.pool': 'Pool',\r\n 'palette.structural.lane': 'Couloir',\r\n 'palette.section.structure': 'Structure',\r\n 'palette.category.events': 'Événements',\r\n 'palette.category.tasks': 'Tâches',\r\n 'palette.category.gateways': 'Passerelles',\r\n 'palette.category.containers': 'Conteneurs',\r\n 'palette.aria.toolbar': 'Palette de formes BPMN',\r\n 'palette.flyout.variantsSuffix': 'Variantes de {label}',\r\n 'palette.toggle.title': 'Basculer la palette large',\r\n 'toolbar.aria.controls': 'Contrôles de l\\'éditeur BPMN',\r\n 'toolbar.undo': 'Annuler',\r\n 'toolbar.redo': 'Rétablir',\r\n 'toolbar.zoomOut': 'Zoom arrière',\r\n 'toolbar.fit': 'Ajuster',\r\n 'toolbar.zoomIn': 'Zoom avant',\r\n 'toolbar.routeCleanup': 'Nettoyer les tracés',\r\n 'toolbar.validation': 'Afficher le rapport de validation',\r\n 'toolbar.documentation': 'Afficher l\\'aperçu de la documentation du processus',\r\n 'toolbar.templates': 'Ouvrir la galerie de modèles rapides',\r\n 'contextPad.addLaneBelow': 'Ajouter un couloir en dessous',\r\n 'contextPad.addLaneAbove': 'Ajouter un couloir au-dessus',\r\n 'contextPad.distributeLanes': 'Répartir les couloirs uniformément',\r\n 'contextPad.deletePool': 'Supprimer le pool',\r\n 'contextPad.deleteLane': 'Supprimer le couloir',\r\n 'contextPad.delete': 'Supprimer',\r\n 'contextPad.appendElement': 'Ajouter un élément...',\r\n 'contextPad.appendTaskAndConnect': 'Ajouter une tâche et connecter',\r\n 'contextPad.attachBoundary': 'Attacher un événement de bordure...',\r\n 'boundaryKind.message': 'Message',\r\n 'boundaryKind.timer': 'Minuterie',\r\n 'boundaryKind.error': 'Erreur',\r\n 'boundaryKind.escalation': 'Escalade',\r\n 'boundaryKind.signal': 'Signal',\r\n 'boundaryKind.conditional': 'Conditionnel',\r\n 'boundaryKind.compensation': 'Compensation',\r\n 'propertyPanel.aria.panel': 'Détails de l\\'élément sélectionné',\r\n 'propertyPanel.tierBadge': 'Niveau {tier}',\r\n 'propertyPanel.eventLabel': 'Événement',\r\n 'propertyPanel.loopLabel': 'Boucle',\r\n 'propertyPanel.namePlaceholder': '(sans nom)',\r\n 'propertyPanel.conditionLabel': 'Condition',\r\n 'propertyPanel.conditionPlaceholder': 'p. ex. ${amount > 100}',\r\n 'propertyPanel.defaultFlowFrom': 'Flux par défaut depuis {source}',\r\n 'propertyPanel.itemCountOne': '{count} élément',\r\n 'propertyPanel.itemCountMany': '{count} éléments',\r\n 'appendMenu.aria.default': 'Ajouter un élément',\r\n 'appendMenu.search.aria': 'Filtrer les types d\\'éléments',\r\n 'appendMenu.search.placeholderFiltered': 'Filtrer...',\r\n 'appendMenu.search.placeholder': 'Rechercher un type d\\'élément...',\r\n 'appendMenu.empty': 'Aucun résultat.',\r\n 'appendMenu.section.tasks': 'Tâches',\r\n 'appendMenu.section.gateways': 'Passerelles',\r\n 'appendMenu.section.events': 'Événements',\r\n 'appendMenu.section.containers': 'Conteneurs',\r\n 'appendMenu.section.data': 'Données',\r\n 'appendMenu.section.other': 'Autre',\r\n 'quickPattern.aria.menu': 'Modèles BPMN rapides',\r\n 'quickPattern.title': 'Modèles rapides',\r\n 'quickPattern.approval.label': 'Flux d\\'approbation',\r\n 'quickPattern.approval.desc': 'Tâche utilisateur suivie d\\'un point de décision.',\r\n 'quickPattern.decision.label': 'Branche de décision',\r\n 'quickPattern.decision.desc': 'Passerelle exclusive avec deux branches de tâches sortantes.',\r\n 'quickPattern.parallelSplitJoin.label': 'Division/jonction parallèle',\r\n 'quickPattern.parallelSplitJoin.desc': 'Passerelle parallèle, deux branches et une passerelle de jonction.',\r\n 'quickPattern.exceptionPath.label': 'Chemin d\\'exception',\r\n 'quickPattern.exceptionPath.desc': 'Tâche suivie d\\'un chemin d\\'événement d\\'exception.',\r\n 'quickPattern.timerEscalation.label': 'Escalade par minuterie',\r\n 'quickPattern.timerEscalation.desc': 'Tâche suivie d\\'un événement intermédiaire de minuterie.',\r\n 'reportPanel.close.aria': 'Fermer le rapport',\r\n 'minimap.aria': 'Mini-carte de vue d\\'ensemble du diagramme',\r\n 'warnings.partialViewOne': 'Vue partielle : {count} vue détaillée non rendue',\r\n 'warnings.partialViewMany': 'Vue partielle : {count} vues détaillées non rendues',\r\n 'warnings.unplacedOne': '{count} élément sans données de position - non affiché',\r\n 'warnings.unplacedMany': '{count} éléments sans données de position - non affichés',\r\n 'warnings.unsupportedTypes': 'Types d\\'éléments non pris en charge affichés comme espaces réservés : {types}{more}',\r\n 'warnings.moreSuffix': ' (+{count} de plus)',\r\n 'warnings.titleOne': '1 notification',\r\n 'warnings.titleMany': '{count} notifications',\r\n 'severity.error': 'Erreur',\r\n 'severity.warning': 'Avertissement',\r\n 'severity.info': 'Info',\r\n 'lint.containerWord.process': 'Processus',\r\n 'lint.containerWord.subProcess': 'Sous-processus',\r\n 'lint.direction.diverging': 'Divergente',\r\n 'lint.direction.converging': 'Convergente',\r\n 'lint.direction.mixed': 'Mixte',\r\n 'lint.startEventRequired': '{container} \\'{label}\\' n\\'a pas d\\'événement de début.',\r\n 'lint.endEventRequired': '{container} \\'{label}\\' n\\'a pas d\\'événement de fin.',\r\n 'lint.singleBlankStartEvent': 'Le processus \\'{label}\\' comporte plusieurs événements de début vierges ; typez chaque point d\\'entrée ou fusionnez-les.',\r\n 'lint.noDisconnected': '{type} \\'{label}\\' n\\'a aucun flux entrant ou sortant.',\r\n 'lint.deadEnd': '{type} \\'{label}\\' est une impasse (aucun flux de séquence sortant).',\r\n 'lint.unreachable': '{type} \\'{label}\\' est inaccessible (aucun flux de séquence entrant).',\r\n 'lint.danglingFlow.source': 'Le flux de séquence \\'{flow}\\' a une source non résolue \\'{ref}\\'.',\r\n 'lint.danglingFlow.target': 'Le flux de séquence \\'{flow}\\' a une cible non résolue \\'{ref}\\'.',\r\n 'lint.danglingFlow.empty': '(vide)',\r\n 'lint.duplicateId': 'L\\'identifiant d\\'élément \\'{id}\\' apparaît {count} fois dans le document.',\r\n 'lint.noImplicitStart': '{type} \\'{label}\\' agit comme un début implicite ; ajoutez un événement de début explicite en amont.',\r\n 'lint.noImplicitEnd': '{type} \\'{label}\\' agit comme une fin implicite ; ajoutez un événement de fin explicite en aval.',\r\n 'lint.noImplicitSplit': '{type} \\'{label}\\' a {count} flux sortants (division implicite) ; utilisez une passerelle explicite.',\r\n 'lint.fakeJoin': '{type} \\'{label}\\' a {count} flux entrants (fausse jonction) ; utilisez une passerelle de jonction explicite.',\r\n 'lint.noGatewayJoinFork': '{type} \\'{label}\\' à la fois joint ({incoming} entrant) et divise ({outgoing} sortant) ; divisez-le en deux passerelles.',\r\n 'lint.noDuplicateSequenceFlows': 'Le flux de séquence \\'{flow}\\' duplique le flux \\'{first}\\' (même source et même cible).',\r\n 'lint.superfluousGateway': '{type} \\'{label}\\' a une entrée et une sortie - il ne joint ni ne divise et peut être supprimé.',\r\n 'lint.gatewayMismatch.diverging': '{type} \\'{label}\\' est déclaré {direction} mais a {count} flux sortant(s).',\r\n 'lint.gatewayMismatch.converging': '{type} \\'{label}\\' est déclaré {direction} mais a {count} flux entrant(s).',\r\n 'lint.gatewayMismatch.mixed': '{type} \\'{label}\\' est déclaré {direction} mais a {incoming} flux entrant(s) / {outgoing} sortant(s).',\r\n 'lint.eventBasedGateway': 'La passerelle basée sur les événements \\'{label}\\' a un successeur de type {type} ; seuls les événements intermédiaires de réception et les tâches de réception sont autorisés.',\r\n 'lint.subProcessBlankStartEvent': 'L\\'événement de début \\'{label}\\' dans le sous-processus \\'{container}\\' est typé ; les sous-processus intégrés doivent utiliser des événements de début vierges.',\r\n 'lint.eventSubProcessTypedStartEvent': 'L\\'événement de début \\'{label}\\' dans le sous-processus d\\'événement \\'{container}\\' est vierge ; les sous-processus d\\'événement nécessitent un début typé.',\r\n 'lint.conditionalFlows': 'Le flux sortant \\'{flow}\\' de {label} nécessite une condition (les flux voisins sont conditionnels).',\r\n 'lint.labelRequired': '{type} \\'{id}\\' n\\'a pas de nom.',\r\n 'lint.noComplexGateway': 'La passerelle complexe \\'{label}\\' est difficile à appréhender ; préférez des passerelles exclusives, parallèles ou inclusives.',\r\n 'lint.noInclusiveGateway': 'La passerelle inclusive \\'{label}\\' (sémantique OU) est plus difficile à appréhender ; envisagez exclusive ou parallèle.',\r\n 'lint.superfluousTermination': 'L\\'événement de fin de terminaison \\'{label}\\' est redondant - une seule fin de terminaison annule toute activité parallèle.',\r\n 'lint.verbObjectActivityLabel': 'L\\'activité \\'{label}\\' devrait commencer par un verbe d\\'action (p. ex. « Approuver la facture », « Envoyer la notification »).',\r\n 'lint.pastParticipleEventLabel': 'L\\'événement \\'{label}\\' devrait être libellé comme un état achevé (p. ex. « Facture reçue », « Commande expédiée »).',\r\n 'lint.decomposeLargeProcess': 'Le processus \\'{label}\\' a {count} nœuds de flux (seuil {threshold}) ; envisagez de le décomposer en sous-processus.',\r\n 'lint.minimizeRoutingPaths.outgoing': '{type} \\'{label}\\' a {count} chemins sortants (seuil {threshold}) ; envisagez de décomposer la ramification.',\r\n 'lint.minimizeRoutingPaths.incoming': '{type} \\'{label}\\' a {count} chemins entrants (seuil {threshold}) ; envisagez de fusionner en amont.',\r\n 'lint.quickFix.removeGateway': 'Supprimer la passerelle',\r\n 'lint.quickFix.deleteElement': 'Supprimer l\\'élément',\r\n 'lint.quickFix.deleteDuplicateFlow': 'Supprimer le flux en double',\r\n 'replaceMenu.title': 'Changer le type en...',\r\n 'report.documentation.title': 'Aperçu de la documentation du processus',\r\n 'report.routeCleanup.title': 'Nettoyer les tracés',\r\n 'report.validation.title': 'Rapport de validation BPMN',\r\n 'report.validation.count': 'Problèmes de validation : {count}',\r\n 'report.validation.none': 'Aucun problème de validation BPMN trouvé.',\r\n};\r\n","/**\r\n * mindzie.BpmnEditor - es message catalog\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: the es translation catalog for the editor chrome.\r\n * GENERATED FILE - do not edit by hand; regenerate via the i18n catalog\r\n * build step. English is the complete source of truth for the key set;\r\n * element-type and toolbar/property terms are reused from the Blazor\r\n * BpmStudioResources .resx so the editor matches the surrounding UI.\r\n * Other strings are machine-authored and pending native review. Keys\r\n * absent from a non-English catalog fall back to English.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { MessageCatalog } from '../types.js';\r\n\r\nexport const es: MessageCatalog = {\r\n 'type.startEvent': 'Evento de inicio',\r\n 'type.endEvent': 'Evento de fin',\r\n 'type.intermediateThrowEvent': 'Evento intermedio de lanzamiento',\r\n 'type.intermediateCatchEvent': 'Evento intermedio de captura',\r\n 'type.boundaryEvent': 'Evento de límite',\r\n 'type.task': 'Tarea',\r\n 'type.userTask': 'Tarea de usuario',\r\n 'type.serviceTask': 'Tarea de servicio',\r\n 'type.sendTask': 'Tarea de envío',\r\n 'type.receiveTask': 'Tarea de recepción',\r\n 'type.manualTask': 'Tarea manual',\r\n 'type.businessRuleTask': 'Tarea de regla de negocio',\r\n 'type.scriptTask': 'Tarea de script',\r\n 'type.callActivity': 'Actividad de llamada',\r\n 'type.exclusiveGateway': 'Puerta de enlace exclusiva',\r\n 'type.parallelGateway': 'Puerta de enlace paralela',\r\n 'type.inclusiveGateway': 'Puerta de enlace inclusiva',\r\n 'type.eventBasedGateway': 'Puerta de enlace basada en eventos',\r\n 'type.complexGateway': 'Puerta de enlace compleja',\r\n 'type.subProcess': 'Subproceso',\r\n 'type.adHocSubProcess': 'Subproceso',\r\n 'type.transaction': 'Subproceso',\r\n 'type.dataObject': 'Objeto de datos',\r\n 'type.dataObjectReference': 'Referencia de objeto de datos',\r\n 'type.dataObject.description': 'Los datos en sí, definidos una sola vez en el proceso.',\r\n 'type.dataObjectReference.description':\r\n 'Apunta a un objeto de datos existente. BPMN dibuja ambos con el mismo símbolo de página.',\r\n 'type.dataStoreReference': 'Referencia de almacén de datos',\r\n 'type.textAnnotation': 'Anotación de texto',\r\n 'type.group': 'Grupo',\r\n 'type.participant': 'Pool',\r\n 'type.lane': 'Carril',\r\n 'type.sequenceFlow': 'Flujo de secuencia',\r\n 'type.messageFlow': 'Flujo de mensaje',\r\n 'type.association': 'Asociación',\r\n 'palette.group.start': 'Evento de inicio',\r\n 'palette.group.intermediate': 'Evento intermedio',\r\n 'palette.group.end': 'Evento de fin',\r\n 'palette.group.task': 'Tarea',\r\n 'palette.group.gateway': 'Puerta de enlace',\r\n 'palette.group.subprocess': 'Subproceso',\r\n 'palette.group.data': 'Objeto de datos',\r\n 'palette.group.annotation': 'Anotación de texto',\r\n 'palette.structural.pool': 'Pool',\r\n 'palette.structural.lane': 'Carril',\r\n 'palette.section.structure': 'Estructura',\r\n 'palette.category.events': 'Eventos',\r\n 'palette.category.tasks': 'Tareas',\r\n 'palette.category.gateways': 'Puertas de enlace',\r\n 'palette.category.containers': 'Contenedores',\r\n 'palette.aria.toolbar': 'Paleta de formas BPMN',\r\n 'palette.flyout.variantsSuffix': 'Variantes de {label}',\r\n 'palette.toggle.title': 'Alternar paleta ancha',\r\n 'toolbar.aria.controls': 'Controles del editor BPMN',\r\n 'toolbar.undo': 'Deshacer',\r\n 'toolbar.redo': 'Rehacer',\r\n 'toolbar.zoomOut': 'Alejar',\r\n 'toolbar.fit': 'Ajustar',\r\n 'toolbar.zoomIn': 'Acercar',\r\n 'toolbar.routeCleanup': 'Limpiar rutas',\r\n 'toolbar.validation': 'Mostrar informe de validación',\r\n 'toolbar.documentation': 'Mostrar vista previa de la documentación del proceso',\r\n 'toolbar.templates': 'Abrir galería de patrones rápidos',\r\n 'contextPad.addLaneBelow': 'Agregar carril debajo',\r\n 'contextPad.addLaneAbove': 'Agregar carril encima',\r\n 'contextPad.distributeLanes': 'Distribuir carriles uniformemente',\r\n 'contextPad.deletePool': 'Eliminar pool',\r\n 'contextPad.deleteLane': 'Eliminar carril',\r\n 'contextPad.delete': 'Eliminar',\r\n 'contextPad.appendElement': 'Anexar elemento...',\r\n 'contextPad.appendTaskAndConnect': 'Anexar tarea y conectar',\r\n 'contextPad.attachBoundary': 'Adjuntar evento de límite...',\r\n 'boundaryKind.message': 'Mensaje',\r\n 'boundaryKind.timer': 'Temporizador',\r\n 'boundaryKind.error': 'Error',\r\n 'boundaryKind.escalation': 'Escalado',\r\n 'boundaryKind.signal': 'Señal',\r\n 'boundaryKind.conditional': 'Condicional',\r\n 'boundaryKind.compensation': 'Compensación',\r\n 'propertyPanel.aria.panel': 'Detalles del elemento seleccionado',\r\n 'propertyPanel.tierBadge': 'Nivel {tier}',\r\n 'propertyPanel.eventLabel': 'Evento',\r\n 'propertyPanel.loopLabel': 'Bucle',\r\n 'propertyPanel.namePlaceholder': '(sin nombre)',\r\n 'propertyPanel.conditionLabel': 'Condición',\r\n 'propertyPanel.conditionPlaceholder': 'p. ej. ${amount > 100}',\r\n 'propertyPanel.defaultFlowFrom': 'Flujo predeterminado desde {source}',\r\n 'propertyPanel.itemCountOne': '{count} elemento',\r\n 'propertyPanel.itemCountMany': '{count} elementos',\r\n 'appendMenu.aria.default': 'Anexar elemento',\r\n 'appendMenu.search.aria': 'Filtrar tipos de elementos',\r\n 'appendMenu.search.placeholderFiltered': 'Filtrar...',\r\n 'appendMenu.search.placeholder': 'Buscar tipo de elemento...',\r\n 'appendMenu.empty': 'Sin coincidencias.',\r\n 'appendMenu.section.tasks': 'Tareas',\r\n 'appendMenu.section.gateways': 'Puertas de enlace',\r\n 'appendMenu.section.events': 'Eventos',\r\n 'appendMenu.section.containers': 'Contenedores',\r\n 'appendMenu.section.data': 'Datos',\r\n 'appendMenu.section.other': 'Otros',\r\n 'quickPattern.aria.menu': 'Patrones BPMN rápidos',\r\n 'quickPattern.title': 'Patrones rápidos',\r\n 'quickPattern.approval.label': 'Flujo de aprobación',\r\n 'quickPattern.approval.desc': 'Tarea de usuario seguida de un punto de decisión.',\r\n 'quickPattern.decision.label': 'Rama de decisión',\r\n 'quickPattern.decision.desc': 'Puerta de enlace exclusiva con dos ramas de tareas salientes.',\r\n 'quickPattern.parallelSplitJoin.label': 'División/unión paralela',\r\n 'quickPattern.parallelSplitJoin.desc': 'Puerta de enlace paralela, dos ramas y una puerta de enlace de unión.',\r\n 'quickPattern.exceptionPath.label': 'Ruta de excepción',\r\n 'quickPattern.exceptionPath.desc': 'Tarea seguida de una ruta de evento de excepción.',\r\n 'quickPattern.timerEscalation.label': 'Escalada por temporizador',\r\n 'quickPattern.timerEscalation.desc': 'Tarea seguida de un evento intermedio de temporizador.',\r\n 'reportPanel.close.aria': 'Cerrar informe',\r\n 'minimap.aria': 'Minimapa de vista general del diagrama',\r\n 'warnings.partialViewOne': 'Vista parcial: {count} vista detallada no representada',\r\n 'warnings.partialViewMany': 'Vista parcial: {count} vistas detalladas no representadas',\r\n 'warnings.unplacedOne': '{count} elemento sin datos de posición - no mostrado',\r\n 'warnings.unplacedMany': '{count} elementos sin datos de posición - no mostrados',\r\n 'warnings.unsupportedTypes': 'Tipos de elementos no compatibles mostrados como marcadores de posición: {types}{more}',\r\n 'warnings.moreSuffix': ' (+{count} más)',\r\n 'warnings.titleOne': '1 aviso',\r\n 'warnings.titleMany': '{count} avisos',\r\n 'severity.error': 'Error',\r\n 'severity.warning': 'Advertencia',\r\n 'severity.info': 'Información',\r\n 'lint.containerWord.process': 'Proceso',\r\n 'lint.containerWord.subProcess': 'Subproceso',\r\n 'lint.direction.diverging': 'Divergente',\r\n 'lint.direction.converging': 'Convergente',\r\n 'lint.direction.mixed': 'Mixto',\r\n 'lint.startEventRequired': '{container} \\'{label}\\' no tiene evento de inicio.',\r\n 'lint.endEventRequired': '{container} \\'{label}\\' no tiene evento de fin.',\r\n 'lint.singleBlankStartEvent': 'El proceso \\'{label}\\' tiene varios eventos de inicio en blanco; tipifique cada punto de entrada o combínelos.',\r\n 'lint.noDisconnected': '{type} \\'{label}\\' no tiene flujos entrantes ni salientes.',\r\n 'lint.deadEnd': '{type} \\'{label}\\' es un callejón sin salida (sin flujo de secuencia saliente).',\r\n 'lint.unreachable': '{type} \\'{label}\\' es inalcanzable (sin flujo de secuencia entrante).',\r\n 'lint.danglingFlow.source': 'El flujo de secuencia \\'{flow}\\' tiene un origen sin resolver \\'{ref}\\'.',\r\n 'lint.danglingFlow.target': 'El flujo de secuencia \\'{flow}\\' tiene un destino sin resolver \\'{ref}\\'.',\r\n 'lint.danglingFlow.empty': '(vacío)',\r\n 'lint.duplicateId': 'El id de elemento \\'{id}\\' aparece {count} veces en el documento.',\r\n 'lint.noImplicitStart': '{type} \\'{label}\\' actúa como un inicio implícito; agregue un evento de inicio explícito aguas arriba.',\r\n 'lint.noImplicitEnd': '{type} \\'{label}\\' actúa como un fin implícito; agregue un evento de fin explícito aguas abajo.',\r\n 'lint.noImplicitSplit': '{type} \\'{label}\\' tiene {count} flujos salientes (división implícita); use una puerta de enlace explícita.',\r\n 'lint.fakeJoin': '{type} \\'{label}\\' tiene {count} flujos entrantes (unión falsa); use una puerta de enlace de unión explícita.',\r\n 'lint.noGatewayJoinFork': '{type} \\'{label}\\' tanto une ({incoming} entrantes) como bifurca ({outgoing} salientes); divídalo en dos puertas de enlace.',\r\n 'lint.noDuplicateSequenceFlows': 'El flujo de secuencia \\'{flow}\\' duplica el flujo \\'{first}\\' (mismo origen y destino).',\r\n 'lint.superfluousGateway': '{type} \\'{label}\\' tiene una entrada y una salida: no une ni bifurca y se puede eliminar.',\r\n 'lint.gatewayMismatch.diverging': '{type} \\'{label}\\' está declarado como {direction} pero tiene {count} flujo(s) saliente(s).',\r\n 'lint.gatewayMismatch.converging': '{type} \\'{label}\\' está declarado como {direction} pero tiene {count} flujo(s) entrante(s).',\r\n 'lint.gatewayMismatch.mixed': '{type} \\'{label}\\' está declarado como {direction} pero tiene {incoming} flujo(s) entrante(s) / {outgoing} saliente(s).',\r\n 'lint.eventBasedGateway': 'La puerta de enlace basada en eventos \\'{label}\\' tiene un sucesor de tipo {type}; solo se permiten eventos intermedios de captura y tareas de recepción.',\r\n 'lint.subProcessBlankStartEvent': 'El evento de inicio \\'{label}\\' dentro del subproceso \\'{container}\\' está tipificado; los subprocesos incrustados deben usar eventos de inicio en blanco.',\r\n 'lint.eventSubProcessTypedStartEvent': 'El evento de inicio \\'{label}\\' en el subproceso de evento \\'{container}\\' está en blanco; los subprocesos de evento requieren un inicio tipificado.',\r\n 'lint.conditionalFlows': 'El flujo saliente \\'{flow}\\' de {label} necesita una condición (los flujos hermanos son condicionales).',\r\n 'lint.labelRequired': '{type} \\'{id}\\' no tiene nombre.',\r\n 'lint.noComplexGateway': 'La puerta de enlace compleja \\'{label}\\' es difícil de interpretar; prefiera puertas de enlace exclusivas, paralelas o inclusivas.',\r\n 'lint.noInclusiveGateway': 'La puerta de enlace inclusiva \\'{label}\\' (semántica O) es más difícil de interpretar; considere exclusiva o paralela.',\r\n 'lint.superfluousTermination': 'El evento de fin de terminación \\'{label}\\' es redundante: una sola terminación cancela toda la actividad paralela.',\r\n 'lint.verbObjectActivityLabel': 'La actividad \\'{label}\\' debe comenzar con un verbo de acción (p. ej. «Aprobar factura», «Enviar notificación»).',\r\n 'lint.pastParticipleEventLabel': 'El evento \\'{label}\\' debe etiquetarse como un estado completado (p. ej. «Factura recibida», «Pedido enviado»).',\r\n 'lint.decomposeLargeProcess': 'El proceso \\'{label}\\' tiene {count} nodos de flujo (umbral {threshold}); considere descomponerlo en subprocesos.',\r\n 'lint.minimizeRoutingPaths.outgoing': '{type} \\'{label}\\' tiene {count} rutas salientes (umbral {threshold}); considere descomponer la ramificación.',\r\n 'lint.minimizeRoutingPaths.incoming': '{type} \\'{label}\\' tiene {count} rutas entrantes (umbral {threshold}); considere fusionar aguas arriba.',\r\n 'lint.quickFix.removeGateway': 'Quitar puerta de enlace',\r\n 'lint.quickFix.deleteElement': 'Eliminar elemento',\r\n 'lint.quickFix.deleteDuplicateFlow': 'Eliminar flujo duplicado',\r\n 'replaceMenu.title': 'Cambiar tipo a...',\r\n 'report.documentation.title': 'Vista previa de la documentación del proceso',\r\n 'report.routeCleanup.title': 'Limpiar rutas',\r\n 'report.validation.title': 'Informe de validación BPMN',\r\n 'report.validation.count': 'Problemas de validación: {count}',\r\n 'report.validation.none': 'No se encontraron problemas de validación BPMN.',\r\n};\r\n","/**\r\n * mindzie.BpmnEditor - nl message catalog\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: the nl translation catalog for the editor chrome.\r\n * GENERATED FILE - do not edit by hand; regenerate via the i18n catalog\r\n * build step. English is the complete source of truth for the key set;\r\n * element-type and toolbar/property terms are reused from the Blazor\r\n * BpmStudioResources .resx so the editor matches the surrounding UI.\r\n * Other strings are machine-authored and pending native review. Keys\r\n * absent from a non-English catalog fall back to English.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { MessageCatalog } from '../types.js';\r\n\r\nexport const nl: MessageCatalog = {\r\n 'type.startEvent': 'Startgebeurtenis',\r\n 'type.endEvent': 'Eindgebeurtenis',\r\n 'type.intermediateThrowEvent': 'Tussenliggende werpgebeurtenis',\r\n 'type.intermediateCatchEvent': 'Tussenliggende vanggebeurtenis',\r\n 'type.boundaryEvent': 'Randgebeurtenis',\r\n 'type.task': 'Taak',\r\n 'type.userTask': 'Gebruikerstaak',\r\n 'type.serviceTask': 'Servicetaak',\r\n 'type.sendTask': 'Verzendtaak',\r\n 'type.receiveTask': 'Ontvangsttaak',\r\n 'type.manualTask': 'Handmatige taak',\r\n 'type.businessRuleTask': 'Bedrijfsregeltaak',\r\n 'type.scriptTask': 'Scripttaak',\r\n 'type.callActivity': 'Aanroepactiviteit',\r\n 'type.exclusiveGateway': 'Exclusieve gateway',\r\n 'type.parallelGateway': 'Parallelle gateway',\r\n 'type.inclusiveGateway': 'Inclusieve gateway',\r\n 'type.eventBasedGateway': 'Gebeurtenisgebaseerde gateway',\r\n 'type.complexGateway': 'Complexe gateway',\r\n 'type.subProcess': 'Subproces',\r\n 'type.adHocSubProcess': 'Subproces',\r\n 'type.transaction': 'Subproces',\r\n 'type.dataObject': 'Gegevensobject',\r\n 'type.dataObjectReference': 'Gegevensobjectreferentie',\r\n 'type.dataObject.description': 'De gegevens zelf, één keer gedefinieerd in het proces.',\r\n 'type.dataObjectReference.description':\r\n 'Verwijst naar een bestaand gegevensobject. BPMN tekent beide met hetzelfde paginasymbool.',\r\n 'type.dataStoreReference': 'Gegevensopslagreferentie',\r\n 'type.textAnnotation': 'Tekstannotatie',\r\n 'type.group': 'Groep',\r\n 'type.participant': 'Pool',\r\n 'type.lane': 'Zwembaan',\r\n 'type.sequenceFlow': 'Sequentiestroom',\r\n 'type.messageFlow': 'Berichtenstroom',\r\n 'type.association': 'Associatie',\r\n 'palette.group.start': 'Startgebeurtenis',\r\n 'palette.group.intermediate': 'Tussenliggende gebeurtenis',\r\n 'palette.group.end': 'Eindgebeurtenis',\r\n 'palette.group.task': 'Taak',\r\n 'palette.group.gateway': 'Gateway',\r\n 'palette.group.subprocess': 'Subproces',\r\n 'palette.group.data': 'Gegevensobject',\r\n 'palette.group.annotation': 'Tekstannotatie',\r\n 'palette.structural.pool': 'Pool',\r\n 'palette.structural.lane': 'Zwembaan',\r\n 'palette.section.structure': 'Structuur',\r\n 'palette.category.events': 'Gebeurtenissen',\r\n 'palette.category.tasks': 'Taken',\r\n 'palette.category.gateways': 'Gateways',\r\n 'palette.category.containers': 'Containers',\r\n 'palette.aria.toolbar': 'BPMN-vormenpalet',\r\n 'palette.flyout.variantsSuffix': 'Varianten van {label}',\r\n 'palette.toggle.title': 'Breed palet in-/uitschakelen',\r\n 'toolbar.aria.controls': 'BPMN-editorbesturing',\r\n 'toolbar.undo': 'Ongedaan maken',\r\n 'toolbar.redo': 'Opnieuw uitvoeren',\r\n 'toolbar.zoomOut': 'Uitzoomen',\r\n 'toolbar.fit': 'Passend maken',\r\n 'toolbar.zoomIn': 'Inzoomen',\r\n 'toolbar.routeCleanup': 'Routes opschonen',\r\n 'toolbar.validation': 'Validatierapport tonen',\r\n 'toolbar.documentation': 'Voorbeeld van procesdocumentatie tonen',\r\n 'toolbar.templates': 'Galerij met snelle patronen openen',\r\n 'contextPad.addLaneBelow': 'Zwembaan onder toevoegen',\r\n 'contextPad.addLaneAbove': 'Zwembaan boven toevoegen',\r\n 'contextPad.distributeLanes': 'Zwembanen gelijkmatig verdelen',\r\n 'contextPad.deletePool': 'Pool verwijderen',\r\n 'contextPad.deleteLane': 'Zwembaan verwijderen',\r\n 'contextPad.delete': 'Verwijderen',\r\n 'contextPad.appendElement': 'Element toevoegen...',\r\n 'contextPad.appendTaskAndConnect': 'Taak toevoegen en verbinden',\r\n 'contextPad.attachBoundary': 'Randgebeurtenis koppelen...',\r\n 'boundaryKind.message': 'Bericht',\r\n 'boundaryKind.timer': 'Timer',\r\n 'boundaryKind.error': 'Fout',\r\n 'boundaryKind.escalation': 'Escalatie',\r\n 'boundaryKind.signal': 'Signaal',\r\n 'boundaryKind.conditional': 'Voorwaardelijk',\r\n 'boundaryKind.compensation': 'Compensatie',\r\n 'propertyPanel.aria.panel': 'Details van geselecteerd element',\r\n 'propertyPanel.tierBadge': 'Niveau {tier}',\r\n 'propertyPanel.eventLabel': 'Gebeurtenis',\r\n 'propertyPanel.loopLabel': 'Lus',\r\n 'propertyPanel.namePlaceholder': '(naamloos)',\r\n 'propertyPanel.conditionLabel': 'Voorwaarde',\r\n 'propertyPanel.conditionPlaceholder': 'bijv. ${amount > 100}',\r\n 'propertyPanel.defaultFlowFrom': 'Standaardstroom vanaf {source}',\r\n 'propertyPanel.itemCountOne': '{count} item',\r\n 'propertyPanel.itemCountMany': '{count} items',\r\n 'appendMenu.aria.default': 'Element toevoegen',\r\n 'appendMenu.search.aria': 'Elementtypen filteren',\r\n 'appendMenu.search.placeholderFiltered': 'Filteren...',\r\n 'appendMenu.search.placeholder': 'Elementtype zoeken...',\r\n 'appendMenu.empty': 'Geen overeenkomsten.',\r\n 'appendMenu.section.tasks': 'Taken',\r\n 'appendMenu.section.gateways': 'Gateways',\r\n 'appendMenu.section.events': 'Gebeurtenissen',\r\n 'appendMenu.section.containers': 'Containers',\r\n 'appendMenu.section.data': 'Gegevens',\r\n 'appendMenu.section.other': 'Overige',\r\n 'quickPattern.aria.menu': 'Snelle BPMN-patronen',\r\n 'quickPattern.title': 'Snelle patronen',\r\n 'quickPattern.approval.label': 'Goedkeuringsstroom',\r\n 'quickPattern.approval.desc': 'Gebruikerstaak gevolgd door een beslispunt.',\r\n 'quickPattern.decision.label': 'Beslissingstak',\r\n 'quickPattern.decision.desc': 'Exclusieve gateway met twee uitgaande taaktakken.',\r\n 'quickPattern.parallelSplitJoin.label': 'Parallel splitsen/samenvoegen',\r\n 'quickPattern.parallelSplitJoin.desc': 'Parallelle gateway, twee takken en een samenvoeggateway.',\r\n 'quickPattern.exceptionPath.label': 'Uitzonderingspad',\r\n 'quickPattern.exceptionPath.desc': 'Taak gevolgd door een pad met een uitzonderingsgebeurtenis.',\r\n 'quickPattern.timerEscalation.label': 'Timer-escalatie',\r\n 'quickPattern.timerEscalation.desc': 'Taak gevolgd door een tussenliggende timergebeurtenis.',\r\n 'reportPanel.close.aria': 'Rapport sluiten',\r\n 'minimap.aria': 'Overzichtsminimap van diagram',\r\n 'warnings.partialViewOne': 'Gedeeltelijke weergave: {count} inzoomweergave niet weergegeven',\r\n 'warnings.partialViewMany': 'Gedeeltelijke weergave: {count} inzoomweergaven niet weergegeven',\r\n 'warnings.unplacedOne': '{count} element zonder positiegegevens - niet getoond',\r\n 'warnings.unplacedMany': '{count} elementen zonder positiegegevens - niet getoond',\r\n 'warnings.unsupportedTypes': 'Niet-ondersteunde elementtypen weergegeven als tijdelijke aanduidingen: {types}{more}',\r\n 'warnings.moreSuffix': ' (+{count} meer)',\r\n 'warnings.titleOne': '1 melding',\r\n 'warnings.titleMany': '{count} meldingen',\r\n 'severity.error': 'Fout',\r\n 'severity.warning': 'Waarschuwing',\r\n 'severity.info': 'Info',\r\n 'lint.containerWord.process': 'Proces',\r\n 'lint.containerWord.subProcess': 'Subproces',\r\n 'lint.direction.diverging': 'Divergerend',\r\n 'lint.direction.converging': 'Convergerend',\r\n 'lint.direction.mixed': 'Gemengd',\r\n 'lint.startEventRequired': '{container} \\'{label}\\' heeft geen startgebeurtenis.',\r\n 'lint.endEventRequired': '{container} \\'{label}\\' heeft geen eindgebeurtenis.',\r\n 'lint.singleBlankStartEvent': 'Proces \\'{label}\\' heeft meerdere lege startgebeurtenissen; typeer elk ingangspunt of voeg ze samen.',\r\n 'lint.noDisconnected': '{type} \\'{label}\\' heeft geen inkomende of uitgaande stromen.',\r\n 'lint.deadEnd': '{type} \\'{label}\\' is een doodlopend punt (geen uitgaande sequentiestroom).',\r\n 'lint.unreachable': '{type} \\'{label}\\' is onbereikbaar (geen inkomende sequentiestroom).',\r\n 'lint.danglingFlow.source': 'Sequentiestroom \\'{flow}\\' heeft een onopgeloste bron \\'{ref}\\'.',\r\n 'lint.danglingFlow.target': 'Sequentiestroom \\'{flow}\\' heeft een onopgelost doel \\'{ref}\\'.',\r\n 'lint.danglingFlow.empty': '(leeg)',\r\n 'lint.duplicateId': 'Element-id \\'{id}\\' komt {count} keer voor in het document.',\r\n 'lint.noImplicitStart': '{type} \\'{label}\\' fungeert als een impliciet begin; voeg stroomopwaarts een expliciete startgebeurtenis toe.',\r\n 'lint.noImplicitEnd': '{type} \\'{label}\\' fungeert als een impliciet einde; voeg stroomafwaarts een expliciete eindgebeurtenis toe.',\r\n 'lint.noImplicitSplit': '{type} \\'{label}\\' heeft {count} uitgaande stromen (impliciete splitsing); gebruik een expliciete gateway.',\r\n 'lint.fakeJoin': '{type} \\'{label}\\' heeft {count} inkomende stromen (schijnbare samenvoeging); gebruik een expliciete samenvoegende gateway.',\r\n 'lint.noGatewayJoinFork': '{type} \\'{label}\\' voegt zowel samen ({incoming} in) als vertakt ({outgoing} uit); splits dit in twee gateways.',\r\n 'lint.noDuplicateSequenceFlows': 'Sequentiestroom \\'{flow}\\' is een duplicaat van stroom \\'{first}\\' (dezelfde bron en hetzelfde doel).',\r\n 'lint.superfluousGateway': '{type} \\'{label}\\' heeft één in en één uit - het voegt niet samen en vertakt niet en kan worden verwijderd.',\r\n 'lint.gatewayMismatch.diverging': '{type} \\'{label}\\' is gedeclareerd als {direction} maar heeft {count} uitgaande stroom/stromen.',\r\n 'lint.gatewayMismatch.converging': '{type} \\'{label}\\' is gedeclareerd als {direction} maar heeft {count} inkomende stroom/stromen.',\r\n 'lint.gatewayMismatch.mixed': '{type} \\'{label}\\' is gedeclareerd als {direction} maar heeft {incoming} inkomende / {outgoing} uitgaande stroom/stromen.',\r\n 'lint.eventBasedGateway': 'Gebeurtenisgebaseerde gateway \\'{label}\\' heeft een opvolger van het type {type}; alleen tussenliggende vanggebeurtenissen en ontvangsttaken zijn toegestaan.',\r\n 'lint.subProcessBlankStartEvent': 'Startgebeurtenis \\'{label}\\' in subproces \\'{container}\\' is getypeerd; ingesloten subprocessen moeten lege startgebeurtenissen gebruiken.',\r\n 'lint.eventSubProcessTypedStartEvent': 'Startgebeurtenis \\'{label}\\' in gebeurtenissubproces \\'{container}\\' is leeg; gebeurtenissubprocessen vereisen een getypeerde start.',\r\n 'lint.conditionalFlows': 'Uitgaande stroom \\'{flow}\\' vanaf {label} heeft een voorwaarde nodig (verwante stromen zijn voorwaardelijk).',\r\n 'lint.labelRequired': '{type} \\'{id}\\' heeft geen naam.',\r\n 'lint.noComplexGateway': 'Complexe gateway \\'{label}\\' is moeilijk te doorgronden; geef de voorkeur aan exclusieve, parallelle of inclusieve gateways.',\r\n 'lint.noInclusiveGateway': 'Inclusieve gateway \\'{label}\\' (OF-semantiek) is moeilijker te doorgronden; overweeg exclusief of parallel.',\r\n 'lint.superfluousTermination': 'Beëindigingseindgebeurtenis \\'{label}\\' is overbodig - één beëindigingseinde annuleert alle parallelle activiteit.',\r\n 'lint.verbObjectActivityLabel': 'Activiteit \\'{label}\\' moet beginnen met een actiewerkwoord (bijv. \"Factuur goedkeuren\", \"Melding verzenden\").',\r\n 'lint.pastParticipleEventLabel': 'Gebeurtenis \\'{label}\\' moet worden gelabeld als een voltooide toestand (bijv. \"Factuur ontvangen\", \"Order verzonden\").',\r\n 'lint.decomposeLargeProcess': 'Proces \\'{label}\\' heeft {count} stroomknooppunten (drempel {threshold}); overweeg het op te delen in subprocessen.',\r\n 'lint.minimizeRoutingPaths.outgoing': '{type} \\'{label}\\' heeft {count} uitgaande paden (drempel {threshold}); overweeg de vertakking op te delen.',\r\n 'lint.minimizeRoutingPaths.incoming': '{type} \\'{label}\\' heeft {count} inkomende paden (drempel {threshold}); overweeg stroomopwaarts samen te voegen.',\r\n 'lint.quickFix.removeGateway': 'Gateway verwijderen',\r\n 'lint.quickFix.deleteElement': 'Element verwijderen',\r\n 'lint.quickFix.deleteDuplicateFlow': 'Dubbele stroom verwijderen',\r\n 'replaceMenu.title': 'Type wijzigen in...',\r\n 'report.documentation.title': 'Voorbeeld van procesdocumentatie',\r\n 'report.routeCleanup.title': 'Routes opschonen',\r\n 'report.validation.title': 'BPMN-validatierapport',\r\n 'report.validation.count': 'Validatieproblemen: {count}',\r\n 'report.validation.none': 'Geen BPMN-validatieproblemen gevonden.',\r\n};\r\n","/**\r\n * mindzie.BpmnEditor - tr message catalog\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: the tr translation catalog for the editor chrome.\r\n * GENERATED FILE - do not edit by hand; regenerate via the i18n catalog\r\n * build step. English is the complete source of truth for the key set;\r\n * element-type and toolbar/property terms are reused from the Blazor\r\n * BpmStudioResources .resx so the editor matches the surrounding UI.\r\n * Other strings are machine-authored and pending native review. Keys\r\n * absent from a non-English catalog fall back to English.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { MessageCatalog } from '../types.js';\r\n\r\nexport const tr: MessageCatalog = {\r\n 'type.startEvent': 'Başlangıç olayı',\r\n 'type.endEvent': 'Bitiş olayı',\r\n 'type.intermediateThrowEvent': 'Ara fırlatma olayı',\r\n 'type.intermediateCatchEvent': 'Ara yakalama olayı',\r\n 'type.boundaryEvent': 'Sınır olayı',\r\n 'type.task': 'Görev',\r\n 'type.userTask': 'Kullanıcı görevi',\r\n 'type.serviceTask': 'Hizmet görevi',\r\n 'type.sendTask': 'Mesaj gönderme görevi',\r\n 'type.receiveTask': 'Mesaj alma görevi',\r\n 'type.manualTask': 'Manuel görev',\r\n 'type.businessRuleTask': 'İş kuralı görevi',\r\n 'type.scriptTask': 'Script görevi',\r\n 'type.callActivity': 'Çağrı etkinliği',\r\n 'type.exclusiveGateway': 'Dışlayıcı ağ geçidi',\r\n 'type.parallelGateway': 'Paralel ağ geçidi',\r\n 'type.inclusiveGateway': 'Kapsayıcı ağ geçidi',\r\n 'type.eventBasedGateway': 'Olay tabanlı ağ geçidi',\r\n 'type.complexGateway': 'Karmaşık ağ geçidi',\r\n 'type.subProcess': 'Alt süreç',\r\n 'type.adHocSubProcess': 'Alt süreç',\r\n 'type.transaction': 'Alt süreç',\r\n 'type.dataObject': 'Veri nesnesi',\r\n 'type.dataObjectReference': 'Veri nesnesi referansı',\r\n 'type.dataObject.description': 'Verinin kendisi; süreçte bir kez tanımlanır.',\r\n 'type.dataObjectReference.description':\r\n 'Var olan bir veri nesnesini işaret eder. BPMN her ikisini de aynı sayfa simgesiyle çizer.',\r\n 'type.dataStoreReference': 'Veri deposu referansı',\r\n 'type.textAnnotation': 'Metin ek açıklaması',\r\n 'type.group': 'Grup',\r\n 'type.participant': 'Havuz',\r\n 'type.lane': 'Kulvar',\r\n 'type.sequenceFlow': 'Sıra akışı',\r\n 'type.messageFlow': 'Mesaj akışı',\r\n 'type.association': 'İlişkilendirme',\r\n 'palette.group.start': 'Başlangıç olayı',\r\n 'palette.group.intermediate': 'Ara olay',\r\n 'palette.group.end': 'Bitiş olayı',\r\n 'palette.group.task': 'Görev',\r\n 'palette.group.gateway': 'Ağ geçidi',\r\n 'palette.group.subprocess': 'Alt süreç',\r\n 'palette.group.data': 'Veri nesnesi',\r\n 'palette.group.annotation': 'Metin ek açıklaması',\r\n 'palette.structural.pool': 'Havuz',\r\n 'palette.structural.lane': 'Kulvar',\r\n 'palette.section.structure': 'Yapı',\r\n 'palette.category.events': 'Olaylar',\r\n 'palette.category.tasks': 'Görevler',\r\n 'palette.category.gateways': 'Ağ geçitleri',\r\n 'palette.category.containers': 'Kapsayıcılar',\r\n 'palette.aria.toolbar': 'BPMN şekil paleti',\r\n 'palette.flyout.variantsSuffix': '{label} çeşitleri',\r\n 'palette.toggle.title': 'Geniş paleti aç/kapat',\r\n 'toolbar.aria.controls': 'BPMN düzenleyici denetimleri',\r\n 'toolbar.undo': 'Geri al',\r\n 'toolbar.redo': 'Yinele',\r\n 'toolbar.zoomOut': 'Uzaklaştır',\r\n 'toolbar.fit': 'Sığdır',\r\n 'toolbar.zoomIn': 'Yakınlaştır',\r\n 'toolbar.routeCleanup': 'Yolları temizle',\r\n 'toolbar.validation': 'Doğrulama raporunu göster',\r\n 'toolbar.documentation': 'Süreç dokümantasyonu önizlemesini göster',\r\n 'toolbar.templates': 'Hızlı desen galerisini aç',\r\n 'contextPad.addLaneBelow': 'Alta kulvar ekle',\r\n 'contextPad.addLaneAbove': 'Üste kulvar ekle',\r\n 'contextPad.distributeLanes': 'Kulvarları eşit dağıt',\r\n 'contextPad.deletePool': 'Havuzu sil',\r\n 'contextPad.deleteLane': 'Kulvarı sil',\r\n 'contextPad.delete': 'Sil',\r\n 'contextPad.appendElement': 'Öğe ekle...',\r\n 'contextPad.appendTaskAndConnect': 'Görev ekle ve bağla',\r\n 'contextPad.attachBoundary': 'Sınır olayı ekle...',\r\n 'boundaryKind.message': 'Mesaj',\r\n 'boundaryKind.timer': 'Zamanlayıcı',\r\n 'boundaryKind.error': 'Hata',\r\n 'boundaryKind.escalation': 'Yükseltme',\r\n 'boundaryKind.signal': 'Sinyal',\r\n 'boundaryKind.conditional': 'Koşullu',\r\n 'boundaryKind.compensation': 'Telafi',\r\n 'propertyPanel.aria.panel': 'Seçili öğe ayrıntıları',\r\n 'propertyPanel.tierBadge': 'Katman {tier}',\r\n 'propertyPanel.eventLabel': 'Olay',\r\n 'propertyPanel.loopLabel': 'Döngü',\r\n 'propertyPanel.namePlaceholder': '(adsız)',\r\n 'propertyPanel.conditionLabel': 'Koşul',\r\n 'propertyPanel.conditionPlaceholder': 'örn. ${amount > 100}',\r\n 'propertyPanel.defaultFlowFrom': '{source} kaynağından varsayılan akış',\r\n 'propertyPanel.itemCountOne': '{count} öğe',\r\n 'propertyPanel.itemCountMany': '{count} öğe',\r\n 'appendMenu.aria.default': 'Öğe ekle',\r\n 'appendMenu.search.aria': 'Öğe türlerini filtrele',\r\n 'appendMenu.search.placeholderFiltered': 'Filtrele...',\r\n 'appendMenu.search.placeholder': 'Öğe türü ara...',\r\n 'appendMenu.empty': 'Eşleşme yok.',\r\n 'appendMenu.section.tasks': 'Görevler',\r\n 'appendMenu.section.gateways': 'Ağ geçitleri',\r\n 'appendMenu.section.events': 'Olaylar',\r\n 'appendMenu.section.containers': 'Kapsayıcılar',\r\n 'appendMenu.section.data': 'Veri',\r\n 'appendMenu.section.other': 'Diğer',\r\n 'quickPattern.aria.menu': 'Hızlı BPMN desenleri',\r\n 'quickPattern.title': 'Hızlı desenler',\r\n 'quickPattern.approval.label': 'Onay akışı',\r\n 'quickPattern.approval.desc': 'Bir karar noktası ile takip edilen kullanıcı görevi.',\r\n 'quickPattern.decision.label': 'Karar dalı',\r\n 'quickPattern.decision.desc': 'İki giden görev dalı olan dışlayıcı ağ geçidi.',\r\n 'quickPattern.parallelSplitJoin.label': 'Paralel ayır/birleştir',\r\n 'quickPattern.parallelSplitJoin.desc': 'Paralel ağ geçidi, iki dal ve bir birleştirme ağ geçidi.',\r\n 'quickPattern.exceptionPath.label': 'İstisna yolu',\r\n 'quickPattern.exceptionPath.desc': 'Bir istisna olay yolu ile takip edilen görev.',\r\n 'quickPattern.timerEscalation.label': 'Zamanlayıcı yükseltme',\r\n 'quickPattern.timerEscalation.desc': 'Bir ara zamanlayıcı olayı ile takip edilen görev.',\r\n 'reportPanel.close.aria': 'Raporu kapat',\r\n 'minimap.aria': 'Diyagrama genel bakış küçük haritası',\r\n 'warnings.partialViewOne': 'Kısmi görünüm: {count} detaylandırma görünümü işlenmedi',\r\n 'warnings.partialViewMany': 'Kısmi görünüm: {count} detaylandırma görünümü işlenmedi',\r\n 'warnings.unplacedOne': 'Konum verisi olmayan {count} öğe - gösterilmiyor',\r\n 'warnings.unplacedMany': 'Konum verisi olmayan {count} öğe - gösterilmiyor',\r\n 'warnings.unsupportedTypes': 'Yer tutucu olarak gösterilen desteklenmeyen öğe türleri: {types}{more}',\r\n 'warnings.moreSuffix': ' (+{count} daha)',\r\n 'warnings.titleOne': '1 bildirim',\r\n 'warnings.titleMany': '{count} bildirim',\r\n 'severity.error': 'Hata',\r\n 'severity.warning': 'Uyarı',\r\n 'severity.info': 'Bilgi',\r\n 'lint.containerWord.process': 'Süreç',\r\n 'lint.containerWord.subProcess': 'Alt süreç',\r\n 'lint.direction.diverging': 'Ayrışan',\r\n 'lint.direction.converging': 'Birleşen',\r\n 'lint.direction.mixed': 'Karma',\r\n 'lint.startEventRequired': '{container} \\'{label}\\' başlangıç olayına sahip değil.',\r\n 'lint.endEventRequired': '{container} \\'{label}\\' bitiş olayına sahip değil.',\r\n 'lint.singleBlankStartEvent': 'Süreç \\'{label}\\' birden fazla boş başlangıç olayına sahip; her giriş noktasını türlendirin veya birleştirin.',\r\n 'lint.noDisconnected': '{type} \\'{label}\\' giden veya gelen akışa sahip değil.',\r\n 'lint.deadEnd': '{type} \\'{label}\\' bir çıkmaz (giden sıralı akış yok).',\r\n 'lint.unreachable': '{type} \\'{label}\\' erişilemez (gelen sıralı akış yok).',\r\n 'lint.danglingFlow.source': 'Sıralı akış \\'{flow}\\' çözümlenemeyen kaynağa \\'{ref}\\' sahip.',\r\n 'lint.danglingFlow.target': 'Sıralı akış \\'{flow}\\' çözümlenemeyen hedefe \\'{ref}\\' sahip.',\r\n 'lint.danglingFlow.empty': '(boş)',\r\n 'lint.duplicateId': 'Öğe kimliği \\'{id}\\' belgede {count} kez görünüyor.',\r\n 'lint.noImplicitStart': '{type} \\'{label}\\' örtük bir başlangıç görevi görüyor; yukarı akışa açık bir başlangıç olayı ekleyin.',\r\n 'lint.noImplicitEnd': '{type} \\'{label}\\' örtük bir bitiş görevi görüyor; aşağı akışa açık bir bitiş olayı ekleyin.',\r\n 'lint.noImplicitSplit': '{type} \\'{label}\\' {count} giden akışa sahip (örtük ayrım); açık bir ağ geçidi kullanın.',\r\n 'lint.fakeJoin': '{type} \\'{label}\\' {count} gelen akışa sahip (sahte birleşim); açık bir birleştirici ağ geçidi kullanın.',\r\n 'lint.noGatewayJoinFork': '{type} \\'{label}\\' hem birleşiyor ({incoming} gelen) hem ayrılıyor ({outgoing} giden); iki ağ geçidine bölün.',\r\n 'lint.noDuplicateSequenceFlows': 'Sıralı akış \\'{flow}\\', \\'{first}\\' akışını yineliyor (aynı kaynak ve hedef).',\r\n 'lint.superfluousGateway': '{type} \\'{label}\\' bir gelen ve bir gidene sahip - ne birleşiyor ne ayrılıyor ve kaldırılabilir.',\r\n 'lint.gatewayMismatch.diverging': '{type} \\'{label}\\' {direction} olarak tanımlı ama {count} giden akışa sahip.',\r\n 'lint.gatewayMismatch.converging': '{type} \\'{label}\\' {direction} olarak tanımlı ama {count} gelen akışa sahip.',\r\n 'lint.gatewayMismatch.mixed': '{type} \\'{label}\\' {direction} olarak tanımlı ama {incoming} gelen / {outgoing} giden akışa sahip.',\r\n 'lint.eventBasedGateway': 'Olay tabanlı ağ geçidi \\'{label}\\' {type} türünde bir ardıla sahip; yalnızca ara yakalama olaylarına ve alma görevlerine izin verilir.',\r\n 'lint.subProcessBlankStartEvent': 'Alt süreç \\'{container}\\' içindeki başlangıç olayı \\'{label}\\' türlendirilmiş; gömülü alt süreçler boş başlangıç olayları kullanmalıdır.',\r\n 'lint.eventSubProcessTypedStartEvent': 'Olay alt süreci \\'{container}\\' içindeki başlangıç olayı \\'{label}\\' boş; olay alt süreçleri türlendirilmiş bir başlangıç gerektirir.',\r\n 'lint.conditionalFlows': '{label} kaynağından giden akış \\'{flow}\\' bir koşula ihtiyaç duyuyor (kardeş akışlar koşullu).',\r\n 'lint.labelRequired': '{type} \\'{id}\\' bir ada sahip değil.',\r\n 'lint.noComplexGateway': 'Karmaşık ağ geçidi \\'{label}\\' anlaşılması zor; dışlayıcı, paralel veya kapsayıcı ağ geçitlerini tercih edin.',\r\n 'lint.noInclusiveGateway': 'Kapsayıcı ağ geçidi \\'{label}\\' (VEYA semantiği) anlaşılması daha zor; dışlayıcı veya paralel düşünün.',\r\n 'lint.superfluousTermination': 'Sonlandırma bitiş olayı \\'{label}\\' gereksiz - bir sonlandırma bitişi tüm paralel etkinliği iptal eder.',\r\n 'lint.verbObjectActivityLabel': 'Etkinlik \\'{label}\\' bir eylem fiiliyle başlamalıdır (örn. \"Faturayı onayla\", \"Bildirim gönder\").',\r\n 'lint.pastParticipleEventLabel': 'Olay \\'{label}\\' tamamlanmış bir durum olarak adlandırılmalıdır (örn. \"Fatura alındı\", \"Sipariş gönderildi\").',\r\n 'lint.decomposeLargeProcess': 'Süreç \\'{label}\\' {count} akış düğümüne sahip (eşik {threshold}); alt süreçlere ayırmayı düşünün.',\r\n 'lint.minimizeRoutingPaths.outgoing': '{type} \\'{label}\\' {count} giden yola sahip (eşik {threshold}); dallanmayı ayırmayı düşünün.',\r\n 'lint.minimizeRoutingPaths.incoming': '{type} \\'{label}\\' {count} gelen yola sahip (eşik {threshold}); yukarı akışta birleştirmeyi düşünün.',\r\n 'lint.quickFix.removeGateway': 'Ağ geçidini kaldır',\r\n 'lint.quickFix.deleteElement': 'Öğeyi sil',\r\n 'lint.quickFix.deleteDuplicateFlow': 'Yinelenen akışı sil',\r\n 'replaceMenu.title': 'Şuna türü değiştir...',\r\n 'report.documentation.title': 'Süreç dokümantasyonu önizlemesi',\r\n 'report.routeCleanup.title': 'Yol temizleme tamamlandı',\r\n 'report.validation.title': 'BPMN doğrulama raporu',\r\n 'report.validation.count': 'Doğrulama sorunları: {count}',\r\n 'report.validation.none': 'BPMN doğrulama sorunu bulunamadı.',\r\n};\r\n","/**\r\n * mindzie.BpmnEditor - ja message catalog\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: the ja translation catalog for the editor chrome.\r\n * GENERATED FILE - do not edit by hand; regenerate via the i18n catalog\r\n * build step. English is the complete source of truth for the key set;\r\n * element-type and toolbar/property terms are reused from the Blazor\r\n * BpmStudioResources .resx so the editor matches the surrounding UI.\r\n * Other strings are machine-authored and pending native review. Keys\r\n * absent from a non-English catalog fall back to English.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { MessageCatalog } from '../types.js';\r\n\r\nexport const ja: MessageCatalog = {\r\n 'type.startEvent': '開始イベント',\r\n 'type.endEvent': '終了イベント',\r\n 'type.intermediateThrowEvent': '中間スローイベント',\r\n 'type.intermediateCatchEvent': '中間キャッチイベント',\r\n 'type.boundaryEvent': '境界イベント',\r\n 'type.task': 'タスク',\r\n 'type.userTask': 'ユーザータスク',\r\n 'type.serviceTask': 'サービスタスク',\r\n 'type.sendTask': '送信タスク',\r\n 'type.receiveTask': '受信タスク',\r\n 'type.manualTask': '手動タスク',\r\n 'type.businessRuleTask': 'ビジネスルールタスク',\r\n 'type.scriptTask': 'スクリプトタスク',\r\n 'type.callActivity': '呼び出しアクティビティ',\r\n 'type.exclusiveGateway': '排他ゲートウェイ',\r\n 'type.parallelGateway': '並列ゲートウェイ',\r\n 'type.inclusiveGateway': '包括ゲートウェイ',\r\n 'type.eventBasedGateway': 'イベントベースゲートウェイ',\r\n 'type.complexGateway': '複合ゲートウェイ',\r\n 'type.subProcess': 'サブプロセス',\r\n 'type.adHocSubProcess': 'サブプロセス',\r\n 'type.transaction': 'サブプロセス',\r\n 'type.dataObject': 'データオブジェクト',\r\n 'type.dataObjectReference': 'データオブジェクト参照',\r\n 'type.dataObject.description': 'データそのもの。プロセス内で一度だけ定義されます。',\r\n 'type.dataObjectReference.description':\r\n '既存のデータオブジェクトを指します。BPMN では両方とも同じページ記号で描画されます。',\r\n 'type.dataStoreReference': 'データストア参照',\r\n 'type.textAnnotation': 'テキスト注釈',\r\n 'type.group': 'グループ',\r\n 'type.participant': 'プール',\r\n 'type.lane': 'レーン',\r\n 'type.sequenceFlow': 'シーケンスフロー',\r\n 'type.messageFlow': 'メッセージフロー',\r\n 'type.association': '関連',\r\n 'palette.group.start': '開始イベント',\r\n 'palette.group.intermediate': '中間イベント',\r\n 'palette.group.end': '終了イベント',\r\n 'palette.group.task': 'タスク',\r\n 'palette.group.gateway': 'ゲートウェイ',\r\n 'palette.group.subprocess': 'サブプロセス',\r\n 'palette.group.data': 'データオブジェクト',\r\n 'palette.group.annotation': 'テキスト注釈',\r\n 'palette.structural.pool': 'プール',\r\n 'palette.structural.lane': 'レーン',\r\n 'palette.section.structure': '構造',\r\n 'palette.category.events': 'イベント',\r\n 'palette.category.tasks': 'タスク',\r\n 'palette.category.gateways': 'ゲートウェイ',\r\n 'palette.category.containers': 'コンテナー',\r\n 'palette.aria.toolbar': 'BPMN シェイプパレット',\r\n 'palette.flyout.variantsSuffix': '{label} のバリエーション',\r\n 'palette.toggle.title': 'ワイドパレットの切り替え',\r\n 'toolbar.aria.controls': 'BPMN エディターコントロール',\r\n 'toolbar.undo': '元に戻す',\r\n 'toolbar.redo': 'やり直し',\r\n 'toolbar.zoomOut': '縮小',\r\n 'toolbar.fit': '全体表示',\r\n 'toolbar.zoomIn': '拡大',\r\n 'toolbar.routeCleanup': 'ルートを整理',\r\n 'toolbar.validation': '検証レポートを表示',\r\n 'toolbar.documentation': 'プロセスドキュメントのプレビューを表示',\r\n 'toolbar.templates': 'クイックパターンギャラリーを開く',\r\n 'contextPad.addLaneBelow': '下にレーンを追加',\r\n 'contextPad.addLaneAbove': '上にレーンを追加',\r\n 'contextPad.distributeLanes': 'レーンを均等に配置',\r\n 'contextPad.deletePool': 'プールを削除',\r\n 'contextPad.deleteLane': 'レーンを削除',\r\n 'contextPad.delete': '削除',\r\n 'contextPad.appendElement': '要素を追加...',\r\n 'contextPad.appendTaskAndConnect': 'タスクを追加して接続',\r\n 'contextPad.attachBoundary': '境界イベントを添付...',\r\n 'boundaryKind.message': 'メッセージ',\r\n 'boundaryKind.timer': 'タイマー',\r\n 'boundaryKind.error': 'エラー',\r\n 'boundaryKind.escalation': 'エスカレーション',\r\n 'boundaryKind.signal': 'シグナル',\r\n 'boundaryKind.conditional': '条件',\r\n 'boundaryKind.compensation': '補償',\r\n 'propertyPanel.aria.panel': '選択した要素の詳細',\r\n 'propertyPanel.tierBadge': 'ティア {tier}',\r\n 'propertyPanel.eventLabel': 'イベント',\r\n 'propertyPanel.loopLabel': 'ループ',\r\n 'propertyPanel.namePlaceholder': '(名前なし)',\r\n 'propertyPanel.conditionLabel': '条件',\r\n 'propertyPanel.conditionPlaceholder': '例: ${amount > 100}',\r\n 'propertyPanel.defaultFlowFrom': '{source} からのデフォルトフロー',\r\n 'propertyPanel.itemCountOne': '{count} 件',\r\n 'propertyPanel.itemCountMany': '{count} 件',\r\n 'appendMenu.aria.default': '要素を追加',\r\n 'appendMenu.search.aria': '要素の種類を絞り込む',\r\n 'appendMenu.search.placeholderFiltered': '絞り込み...',\r\n 'appendMenu.search.placeholder': '要素の種類を検索...',\r\n 'appendMenu.empty': '一致するものがありません。',\r\n 'appendMenu.section.tasks': 'タスク',\r\n 'appendMenu.section.gateways': 'ゲートウェイ',\r\n 'appendMenu.section.events': 'イベント',\r\n 'appendMenu.section.containers': 'コンテナー',\r\n 'appendMenu.section.data': 'データ',\r\n 'appendMenu.section.other': 'その他',\r\n 'quickPattern.aria.menu': 'クイック BPMN パターン',\r\n 'quickPattern.title': 'クイックパターン',\r\n 'quickPattern.approval.label': '承認フロー',\r\n 'quickPattern.approval.desc': 'ユーザータスクの後に決定ポイントが続きます。',\r\n 'quickPattern.decision.label': '決定分岐',\r\n 'quickPattern.decision.desc': '2 つの出力タスク分岐を持つ排他ゲートウェイ。',\r\n 'quickPattern.parallelSplitJoin.label': '並列分岐/結合',\r\n 'quickPattern.parallelSplitJoin.desc': '並列ゲートウェイ、2 つの分岐、および結合ゲートウェイ。',\r\n 'quickPattern.exceptionPath.label': '例外パス',\r\n 'quickPattern.exceptionPath.desc': 'タスクの後に例外イベントパスが続きます。',\r\n 'quickPattern.timerEscalation.label': 'タイマーエスカレーション',\r\n 'quickPattern.timerEscalation.desc': 'タスクの後に中間タイマーイベントが続きます。',\r\n 'reportPanel.close.aria': 'レポートを閉じる',\r\n 'minimap.aria': '図の概要ミニマップ',\r\n 'warnings.partialViewOne': '部分表示: {count} 件のドリルダウンビューが描画されていません',\r\n 'warnings.partialViewMany': '部分表示: {count} 件のドリルダウンビューが描画されていません',\r\n 'warnings.unplacedOne': '位置データのない要素が {count} 件 - 表示されません',\r\n 'warnings.unplacedMany': '位置データのない要素が {count} 件 - 表示されません',\r\n 'warnings.unsupportedTypes': 'サポートされていない要素の種類はプレースホルダーとして表示されます: {types}{more}',\r\n 'warnings.moreSuffix': ' (他 {count} 件)',\r\n 'warnings.titleOne': '1 件の通知',\r\n 'warnings.titleMany': '{count} 件の通知',\r\n 'severity.error': 'エラー',\r\n 'severity.warning': '警告',\r\n 'severity.info': '情報',\r\n 'lint.containerWord.process': 'プロセス',\r\n 'lint.containerWord.subProcess': 'サブプロセス',\r\n 'lint.direction.diverging': '分岐',\r\n 'lint.direction.converging': '結合',\r\n 'lint.direction.mixed': '混在',\r\n 'lint.startEventRequired': '{container} \\'{label}\\' に開始イベントがありません。',\r\n 'lint.endEventRequired': '{container} \\'{label}\\' に終了イベントがありません。',\r\n 'lint.singleBlankStartEvent': 'プロセス \\'{label}\\' に空白の開始イベントが複数あります。各エントリポイントに種類を設定するか、統合してください。',\r\n 'lint.noDisconnected': '{type} \\'{label}\\' に入力フローも出力フローもありません。',\r\n 'lint.deadEnd': '{type} \\'{label}\\' は行き止まりです (出力シーケンスフローがありません)。',\r\n 'lint.unreachable': '{type} \\'{label}\\' に到達できません (入力シーケンスフローがありません)。',\r\n 'lint.danglingFlow.source': 'シーケンスフロー \\'{flow}\\' のソース \\'{ref}\\' が解決されていません。',\r\n 'lint.danglingFlow.target': 'シーケンスフロー \\'{flow}\\' のターゲット \\'{ref}\\' が解決されていません。',\r\n 'lint.danglingFlow.empty': '(空)',\r\n 'lint.duplicateId': '要素 ID \\'{id}\\' がドキュメント内に {count} 回出現します。',\r\n 'lint.noImplicitStart': '{type} \\'{label}\\' は暗黙の開始として機能しています。上流に明示的な開始イベントを追加してください。',\r\n 'lint.noImplicitEnd': '{type} \\'{label}\\' は暗黙の終了として機能しています。下流に明示的な終了イベントを追加してください。',\r\n 'lint.noImplicitSplit': '{type} \\'{label}\\' に {count} 件の出力フローがあります (暗黙の分岐)。明示的なゲートウェイを使用してください。',\r\n 'lint.fakeJoin': '{type} \\'{label}\\' に {count} 件の入力フローがあります (疑似結合)。明示的な結合ゲートウェイを使用してください。',\r\n 'lint.noGatewayJoinFork': '{type} \\'{label}\\' は結合 ({incoming} 入力) と分岐 ({outgoing} 出力) の両方を行っています。2 つのゲートウェイに分割してください。',\r\n 'lint.noDuplicateSequenceFlows': 'シーケンスフロー \\'{flow}\\' はフロー \\'{first}\\' と重複しています (ソースとターゲットが同じ)。',\r\n 'lint.superfluousGateway': '{type} \\'{label}\\' は入力が 1 つ、出力が 1 つです - 結合も分岐もしていないため削除できます。',\r\n 'lint.gatewayMismatch.diverging': '{type} \\'{label}\\' は {direction} と宣言されていますが、{count} 件の出力フローがあります。',\r\n 'lint.gatewayMismatch.converging': '{type} \\'{label}\\' は {direction} と宣言されていますが、{count} 件の入力フローがあります。',\r\n 'lint.gatewayMismatch.mixed': '{type} \\'{label}\\' は {direction} と宣言されていますが、入力 {incoming} 件 / 出力 {outgoing} 件のフローがあります。',\r\n 'lint.eventBasedGateway': 'イベントベースゲートウェイ \\'{label}\\' には種類が {type} の後続要素があります。中間キャッチイベントと受信タスクのみが許可されています。',\r\n 'lint.subProcessBlankStartEvent': 'サブプロセス \\'{container}\\' 内の開始イベント \\'{label}\\' に種類が設定されています。埋め込みサブプロセスは空白の開始イベントを使用する必要があります。',\r\n 'lint.eventSubProcessTypedStartEvent': 'イベントサブプロセス \\'{container}\\' 内の開始イベント \\'{label}\\' が空白です。イベントサブプロセスには種類が設定された開始が必要です。',\r\n 'lint.conditionalFlows': '{label} からの出力フロー \\'{flow}\\' には条件が必要です (兄弟フローが条件付きです)。',\r\n 'lint.labelRequired': '{type} \\'{id}\\' に名前がありません。',\r\n 'lint.noComplexGateway': '複合ゲートウェイ \\'{label}\\' は理解が困難です。排他、並列、または包括ゲートウェイを推奨します。',\r\n 'lint.noInclusiveGateway': '包括ゲートウェイ \\'{label}\\' (OR セマンティクス) は理解がより困難です。排他または並列を検討してください。',\r\n 'lint.superfluousTermination': '強制終了イベント \\'{label}\\' は冗長です - 1 つの強制終了イベントですべての並列アクティビティがキャンセルされます。',\r\n 'lint.verbObjectActivityLabel': 'アクティビティ \\'{label}\\' は動作を表す動詞で始める必要があります (例: \"請求書を承認\"、\"通知を送信\")。',\r\n 'lint.pastParticipleEventLabel': 'イベント \\'{label}\\' は完了した状態として名前を付ける必要があります (例: \"請求書を受領\"、\"注文を出荷\")。',\r\n 'lint.decomposeLargeProcess': 'プロセス \\'{label}\\' に {count} 個のフローノードがあります (しきい値 {threshold})。サブプロセスへの分解を検討してください。',\r\n 'lint.minimizeRoutingPaths.outgoing': '{type} \\'{label}\\' に {count} 件の出力パスがあります (しきい値 {threshold})。分岐の分解を検討してください。',\r\n 'lint.minimizeRoutingPaths.incoming': '{type} \\'{label}\\' に {count} 件の入力パスがあります (しきい値 {threshold})。上流での結合を検討してください。',\r\n 'lint.quickFix.removeGateway': 'ゲートウェイを削除',\r\n 'lint.quickFix.deleteElement': '要素を削除',\r\n 'lint.quickFix.deleteDuplicateFlow': '重複フローを削除',\r\n 'replaceMenu.title': '種類を変更...',\r\n 'report.documentation.title': 'プロセスドキュメントのプレビュー',\r\n 'report.routeCleanup.title': 'ルートを整理',\r\n 'report.validation.title': 'BPMN 検証レポート',\r\n 'report.validation.count': '検証の問題: {count}',\r\n 'report.validation.none': 'BPMN 検証の問題は見つかりませんでした。',\r\n};\r\n","/**\r\n * mindzie.BpmnEditor - da message catalog\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: the da translation catalog for the editor chrome.\r\n * GENERATED FILE - do not edit by hand; regenerate via the i18n catalog\r\n * build step. English is the complete source of truth for the key set;\r\n * element-type and toolbar/property terms are reused from the Blazor\r\n * BpmStudioResources .resx so the editor matches the surrounding UI.\r\n * Other strings are machine-authored and pending native review. Keys\r\n * absent from a non-English catalog fall back to English.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { MessageCatalog } from '../types.js';\r\n\r\nexport const da: MessageCatalog = {\r\n 'type.startEvent': 'Starthændelse',\r\n 'type.endEvent': 'Sluthændelse',\r\n 'type.intermediateThrowEvent': 'Mellemliggende kasthændelse',\r\n 'type.intermediateCatchEvent': 'Mellemliggende fanghændelse',\r\n 'type.boundaryEvent': 'Grænsehændelse',\r\n 'type.task': 'Opgave',\r\n 'type.userTask': 'Brugeropgave',\r\n 'type.serviceTask': 'Serviceopgave',\r\n 'type.sendTask': 'Sendeopgave',\r\n 'type.receiveTask': 'Modtageopgave',\r\n 'type.manualTask': 'Manuel opgave',\r\n 'type.businessRuleTask': 'Forretningsregelopgave',\r\n 'type.scriptTask': 'Scriptopgave',\r\n 'type.callActivity': 'Kaldaktivitet',\r\n 'type.exclusiveGateway': 'Eksklusiv gateway',\r\n 'type.parallelGateway': 'Parallel gateway',\r\n 'type.inclusiveGateway': 'Inklusiv gateway',\r\n 'type.eventBasedGateway': 'Hændelsesbaseret gateway',\r\n 'type.complexGateway': 'Kompleks gateway',\r\n 'type.subProcess': 'Underproces',\r\n 'type.adHocSubProcess': 'Underproces',\r\n 'type.transaction': 'Underproces',\r\n 'type.dataObject': 'Dataobjekt',\r\n 'type.dataObjectReference': 'Dataobjektreference',\r\n 'type.dataObject.description': 'Selve dataene, defineret én gang i processen.',\r\n 'type.dataObjectReference.description':\r\n 'Peger på et eksisterende dataobjekt. BPMN tegner begge med samme sidesymbol.',\r\n 'type.dataStoreReference': 'Datalagerreference',\r\n 'type.textAnnotation': 'Tekstannotation',\r\n 'type.group': 'Gruppe',\r\n 'type.participant': 'Pulje',\r\n 'type.lane': 'Bane',\r\n 'type.sequenceFlow': 'Sekvensflow',\r\n 'type.messageFlow': 'Beskedflow',\r\n 'type.association': 'Tilknytning',\r\n 'palette.group.start': 'Starthændelse',\r\n 'palette.group.intermediate': 'Mellemliggende hændelse',\r\n 'palette.group.end': 'Sluthændelse',\r\n 'palette.group.task': 'Opgave',\r\n 'palette.group.gateway': 'Gateway',\r\n 'palette.group.subprocess': 'Underproces',\r\n 'palette.group.data': 'Dataobjekt',\r\n 'palette.group.annotation': 'Tekstannotation',\r\n 'palette.structural.pool': 'Pulje',\r\n 'palette.structural.lane': 'Bane',\r\n 'palette.section.structure': 'Struktur',\r\n 'palette.category.events': 'Hændelser',\r\n 'palette.category.tasks': 'Opgaver',\r\n 'palette.category.gateways': 'Gateways',\r\n 'palette.category.containers': 'Beholdere',\r\n 'palette.aria.toolbar': 'BPMN-formpalet',\r\n 'palette.flyout.variantsSuffix': 'Varianter af {label}',\r\n 'palette.toggle.title': 'Slå bred palet til/fra',\r\n 'toolbar.aria.controls': 'BPMN-editorkontroller',\r\n 'toolbar.undo': 'Fortryd',\r\n 'toolbar.redo': 'Annuller fortryd',\r\n 'toolbar.zoomOut': 'Zoom ud',\r\n 'toolbar.fit': 'Tilpas',\r\n 'toolbar.zoomIn': 'Zoom ind',\r\n 'toolbar.routeCleanup': 'Ryd op i ruter',\r\n 'toolbar.validation': 'Vis valideringsrapport',\r\n 'toolbar.documentation': 'Vis eksempel på procesdokumentation',\r\n 'toolbar.templates': 'Åbn galleri med hurtige mønstre',\r\n 'contextPad.addLaneBelow': 'Tilføj bane nedenfor',\r\n 'contextPad.addLaneAbove': 'Tilføj bane ovenfor',\r\n 'contextPad.distributeLanes': 'Fordel baner jævnt',\r\n 'contextPad.deletePool': 'Slet pulje',\r\n 'contextPad.deleteLane': 'Slet bane',\r\n 'contextPad.delete': 'Slet',\r\n 'contextPad.appendElement': 'Tilføj element...',\r\n 'contextPad.appendTaskAndConnect': 'Tilføj opgave og forbind',\r\n 'contextPad.attachBoundary': 'Tilknyt grænsehændelse...',\r\n 'boundaryKind.message': 'Meddelelse',\r\n 'boundaryKind.timer': 'Timer',\r\n 'boundaryKind.error': 'Fejl',\r\n 'boundaryKind.escalation': 'Eskalering',\r\n 'boundaryKind.signal': 'Signal',\r\n 'boundaryKind.conditional': 'Betinget',\r\n 'boundaryKind.compensation': 'Kompensation',\r\n 'propertyPanel.aria.panel': 'Detaljer for valgt element',\r\n 'propertyPanel.tierBadge': 'Niveau {tier}',\r\n 'propertyPanel.eventLabel': 'Hændelse',\r\n 'propertyPanel.loopLabel': 'Løkke',\r\n 'propertyPanel.namePlaceholder': '(uden navn)',\r\n 'propertyPanel.conditionLabel': 'Betingelse',\r\n 'propertyPanel.conditionPlaceholder': 'f.eks. ${amount > 100}',\r\n 'propertyPanel.defaultFlowFrom': 'Standardflow fra {source}',\r\n 'propertyPanel.itemCountOne': '{count} element',\r\n 'propertyPanel.itemCountMany': '{count} elementer',\r\n 'appendMenu.aria.default': 'Tilføj element',\r\n 'appendMenu.search.aria': 'Filtrér elementtyper',\r\n 'appendMenu.search.placeholderFiltered': 'Filtrér...',\r\n 'appendMenu.search.placeholder': 'Søg efter elementtype...',\r\n 'appendMenu.empty': 'Ingen match.',\r\n 'appendMenu.section.tasks': 'Opgaver',\r\n 'appendMenu.section.gateways': 'Gateways',\r\n 'appendMenu.section.events': 'Hændelser',\r\n 'appendMenu.section.containers': 'Beholdere',\r\n 'appendMenu.section.data': 'Data',\r\n 'appendMenu.section.other': 'Andet',\r\n 'quickPattern.aria.menu': 'Hurtige BPMN-mønstre',\r\n 'quickPattern.title': 'Hurtige mønstre',\r\n 'quickPattern.approval.label': 'Godkendelsesflow',\r\n 'quickPattern.approval.desc': 'Brugeropgave efterfulgt af et beslutningspunkt.',\r\n 'quickPattern.decision.label': 'Beslutningsgren',\r\n 'quickPattern.decision.desc': 'Eksklusiv gateway med to udgående opgavegrene.',\r\n 'quickPattern.parallelSplitJoin.label': 'Parallel opdeling/sammenføjning',\r\n 'quickPattern.parallelSplitJoin.desc': 'Parallel gateway, to grene og en sammenføjningsgateway.',\r\n 'quickPattern.exceptionPath.label': 'Undtagelsessti',\r\n 'quickPattern.exceptionPath.desc': 'Opgave efterfulgt af en sti med en undtagelseshændelse.',\r\n 'quickPattern.timerEscalation.label': 'Timereskalering',\r\n 'quickPattern.timerEscalation.desc': 'Opgave efterfulgt af en mellemliggende timerhændelse.',\r\n 'reportPanel.close.aria': 'Luk rapport',\r\n 'minimap.aria': 'Oversigtsminimap over diagram',\r\n 'warnings.partialViewOne': 'Delvis visning: {count} detaljevisning blev ikke gengivet',\r\n 'warnings.partialViewMany': 'Delvis visning: {count} detaljevisninger blev ikke gengivet',\r\n 'warnings.unplacedOne': '{count} element uden positionsdata - vises ikke',\r\n 'warnings.unplacedMany': '{count} elementer uden positionsdata - vises ikke',\r\n 'warnings.unsupportedTypes': 'Ikke-understøttede elementtyper vises som pladsholdere: {types}{more}',\r\n 'warnings.moreSuffix': ' (+{count} mere)',\r\n 'warnings.titleOne': '1 notits',\r\n 'warnings.titleMany': '{count} notitser',\r\n 'severity.error': 'Fejl',\r\n 'severity.warning': 'Advarsel',\r\n 'severity.info': 'Info',\r\n 'lint.containerWord.process': 'Proces',\r\n 'lint.containerWord.subProcess': 'Underproces',\r\n 'lint.direction.diverging': 'Divergerende',\r\n 'lint.direction.converging': 'Konvergerende',\r\n 'lint.direction.mixed': 'Blandet',\r\n 'lint.startEventRequired': '{container} \\'{label}\\' har ingen starthændelse.',\r\n 'lint.endEventRequired': '{container} \\'{label}\\' har ingen sluthændelse.',\r\n 'lint.singleBlankStartEvent': 'Proces \\'{label}\\' har flere tomme starthændelser; angiv type for hvert indgangspunkt, eller flet dem sammen.',\r\n 'lint.noDisconnected': '{type} \\'{label}\\' har ingen indgående eller udgående flows.',\r\n 'lint.deadEnd': '{type} \\'{label}\\' er en blind vej (intet udgående sekvensflow).',\r\n 'lint.unreachable': '{type} \\'{label}\\' kan ikke nås (intet indgående sekvensflow).',\r\n 'lint.danglingFlow.source': 'Sekvensflow \\'{flow}\\' har en uafklaret kilde \\'{ref}\\'.',\r\n 'lint.danglingFlow.target': 'Sekvensflow \\'{flow}\\' har et uafklaret mål \\'{ref}\\'.',\r\n 'lint.danglingFlow.empty': '(tom)',\r\n 'lint.duplicateId': 'Element-id \\'{id}\\' forekommer {count} gange i dokumentet.',\r\n 'lint.noImplicitStart': '{type} \\'{label}\\' fungerer som en implicit start; tilføj en eksplicit starthændelse opstrøms.',\r\n 'lint.noImplicitEnd': '{type} \\'{label}\\' fungerer som en implicit slutning; tilføj en eksplicit sluthændelse nedstrøms.',\r\n 'lint.noImplicitSplit': '{type} \\'{label}\\' har {count} udgående flows (implicit opdeling); brug en eksplicit gateway.',\r\n 'lint.fakeJoin': '{type} \\'{label}\\' har {count} indgående flows (falsk sammenføjning); brug en eksplicit sammenføjende gateway.',\r\n 'lint.noGatewayJoinFork': '{type} \\'{label}\\' både sammenføjer ({incoming} ind) og forgrener ({outgoing} ud); opdel den i to gateways.',\r\n 'lint.noDuplicateSequenceFlows': 'Sekvensflow \\'{flow}\\' er et duplikat af flow \\'{first}\\' (samme kilde og mål).',\r\n 'lint.superfluousGateway': '{type} \\'{label}\\' har én ind og én ud - den hverken sammenføjer eller forgrener og kan fjernes.',\r\n 'lint.gatewayMismatch.diverging': '{type} \\'{label}\\' er erklæret {direction}, men har {count} udgående flow(s).',\r\n 'lint.gatewayMismatch.converging': '{type} \\'{label}\\' er erklæret {direction}, men har {count} indgående flow(s).',\r\n 'lint.gatewayMismatch.mixed': '{type} \\'{label}\\' er erklæret {direction}, men har {incoming} indgående / {outgoing} udgående flow(s).',\r\n 'lint.eventBasedGateway': 'Hændelsesbaseret gateway \\'{label}\\' har en efterfølger af typen {type}; kun mellemliggende fanghændelser og modtageopgaver er tilladt.',\r\n 'lint.subProcessBlankStartEvent': 'Starthændelse \\'{label}\\' i underproces \\'{container}\\' har en type; indlejrede underprocesser skal bruge tomme starthændelser.',\r\n 'lint.eventSubProcessTypedStartEvent': 'Starthændelse \\'{label}\\' i hændelsesunderproces \\'{container}\\' er tom; hændelsesunderprocesser kræver en typet start.',\r\n 'lint.conditionalFlows': 'Udgående flow \\'{flow}\\' fra {label} kræver en betingelse (søskendeflows er betingede).',\r\n 'lint.labelRequired': '{type} \\'{id}\\' har ikke noget navn.',\r\n 'lint.noComplexGateway': 'Kompleks gateway \\'{label}\\' er svær at gennemskue; foretræk eksklusive, parallelle eller inklusive gateways.',\r\n 'lint.noInclusiveGateway': 'Inklusiv gateway \\'{label}\\' (ELLER-semantik) er sværere at gennemskue; overvej eksklusiv eller parallel.',\r\n 'lint.superfluousTermination': 'Terminér-sluthændelse \\'{label}\\' er overflødig - én terminér-slutning annullerer al parallel aktivitet.',\r\n 'lint.verbObjectActivityLabel': 'Aktivitet \\'{label}\\' bør begynde med et handlingsverbum (f.eks. \"Godkend faktura\", \"Send notifikation\").',\r\n 'lint.pastParticipleEventLabel': 'Hændelse \\'{label}\\' bør navngives som en fuldført tilstand (f.eks. \"Faktura modtaget\", \"Ordre afsendt\").',\r\n 'lint.decomposeLargeProcess': 'Proces \\'{label}\\' har {count} flowknuder (tærskel {threshold}); overvej at opdele den i underprocesser.',\r\n 'lint.minimizeRoutingPaths.outgoing': '{type} \\'{label}\\' har {count} udgående stier (tærskel {threshold}); overvej at opdele forgreningen.',\r\n 'lint.minimizeRoutingPaths.incoming': '{type} \\'{label}\\' har {count} indgående stier (tærskel {threshold}); overvej at sammenføje opstrøms.',\r\n 'lint.quickFix.removeGateway': 'Fjern gateway',\r\n 'lint.quickFix.deleteElement': 'Slet element',\r\n 'lint.quickFix.deleteDuplicateFlow': 'Slet duplikeret flow',\r\n 'replaceMenu.title': 'Skift type til...',\r\n 'report.documentation.title': 'Eksempel på procesdokumentation',\r\n 'report.routeCleanup.title': 'Ryd op i ruter',\r\n 'report.validation.title': 'BPMN-valideringsrapport',\r\n 'report.validation.count': 'Valideringsproblemer: {count}',\r\n 'report.validation.none': 'Ingen BPMN-valideringsproblemer fundet.',\r\n};\r\n","/**\r\n * mindzie.BpmnEditor - message catalog registry\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: the registry mapping each supported base language code\r\n * to its catalog, plus the default language. `createTranslator`\r\n * resolves a BCP-47 tag to one of these via `normalizeLanguage`.\r\n *\r\n * English is the source of truth for the full key set; every other\r\n * catalog mirrors it. Languages match the app's supported set\r\n * (LanguageHelper.cs): en, de, fr, es, nl, tr, ja, da.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { MessageCatalog } from '../types.js';\r\nimport { en } from './en.js';\r\nimport { de } from './de.js';\r\nimport { fr } from './fr.js';\r\nimport { es } from './es.js';\r\nimport { nl } from './nl.js';\r\nimport { tr } from './tr.js';\r\nimport { ja } from './ja.js';\r\nimport { da } from './da.js';\r\n\r\n/** Supported base language codes (BCP-47 primary subtag). */\r\nexport type SupportedLanguage = 'en' | 'de' | 'fr' | 'es' | 'nl' | 'tr' | 'ja' | 'da';\r\n\r\n/** The source language; used as the resolution fallback. */\r\nexport const DEFAULT_LANGUAGE: SupportedLanguage = 'en';\r\n\r\n/** Every supported language's catalog, keyed by base language code. */\r\nexport const CATALOGS: Record = {\r\n en,\r\n de,\r\n fr,\r\n es,\r\n nl,\r\n tr,\r\n ja,\r\n da,\r\n};\r\n","/**\r\n * mindzie.BpmnEditor - translator factory\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: `createTranslator(language)`, which resolves a BCP-47\r\n * language tag to a catalog and returns a `Translator`. Resolution:\r\n *\r\n * 1. Normalise the tag to its base language (`tr-TR` -> `tr`,\r\n * case-insensitive).\r\n * 2. Use that language's catalog when supported; otherwise use the\r\n * default (English).\r\n * 3. Per lookup: active-language string -> English string -> the key\r\n * itself. This source-language fallback is standard i18n behaviour\r\n * so a not-yet-translated key degrades to readable English rather\r\n * than a blank or `undefined`.\r\n *\r\n * The English catalog is the source of truth for the full key set; the\r\n * other language catalogs are expected to mirror it. `t()` interpolates\r\n * `{name}` placeholders from the supplied params.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { MessageCatalog, MessageParams, Translator } from './types.js';\r\nimport { CATALOGS, DEFAULT_LANGUAGE, type SupportedLanguage } from './messages/index.js';\r\n\r\n/**\r\n * Normalise an arbitrary BCP-47 tag to one of the supported base\r\n * language codes, or the default when unsupported/blank. `en-US`,\r\n * `EN`, and `en` all resolve to `en`; `tr-TR` resolves to `tr`.\r\n */\r\nexport function normalizeLanguage(language: string | undefined | null): SupportedLanguage {\r\n if (typeof language !== 'string' || language.length === 0) {\r\n return DEFAULT_LANGUAGE;\r\n }\r\n const base = language.toLowerCase().split('-')[0] ?? '';\r\n return base in CATALOGS ? (base as SupportedLanguage) : DEFAULT_LANGUAGE;\r\n}\r\n\r\n/**\r\n * Substitute `{name}` placeholders in `template` with values from\r\n * `params`. Unknown placeholders are left verbatim (so an authoring\r\n * mismatch is visible rather than silently blanked). No-op when params\r\n * is absent.\r\n */\r\nfunction interpolate(template: string, params?: MessageParams): string {\r\n if (!params) {\r\n return template;\r\n }\r\n return template.replace(/\\{(\\w+)\\}/g, (match, name: string) => {\r\n const value = params[name];\r\n return value === undefined ? match : String(value);\r\n });\r\n}\r\n\r\n/**\r\n * Build a `Translator` bound to the given language. Created once per\r\n * editor instance from `BpmnEditorOptions.language` and threaded into\r\n * every chrome module.\r\n */\r\nexport function createTranslator(language: string | undefined | null): Translator {\r\n const active: SupportedLanguage = normalizeLanguage(language);\r\n const primary: MessageCatalog = CATALOGS[active];\r\n const fallback: MessageCatalog = CATALOGS[DEFAULT_LANGUAGE];\r\n return (key, params) => {\r\n const template = primary[key] ?? fallback[key] ?? key;\r\n return interpolate(template, params);\r\n };\r\n}\r\n\r\n/**\r\n * A ready-made English translator. Used as the default when a chrome\r\n * module is mounted without an explicit translator (standalone unit\r\n * tests, advanced embedders that never set `language`). This is the\r\n * library's source language, not a degraded fallback path.\r\n */\r\nexport const englishTranslator: Translator = createTranslator(DEFAULT_LANGUAGE);\r\n","/**\r\n * mindzie.BpmnEditor - minimap overview\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: S1 from IMPLEMENTATION_STATUS.md 3.2 - a small SVG\r\n * overview mounted into a host-provided element. Shows a scaled-down\r\n * silhouette of every shape plus a rectangle representing the current\r\n * viewport; clicking the minimap pans the main viewport so the clicked\r\n * scene-space point lands at the centre of the editor container.\r\n *\r\n * Updates on every viewport change via the Viewport listener; the\r\n * layout is captured at mount time and does not change (the facade\r\n * creates a fresh minimap after each rerender, which is cheap).\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { Viewport, ViewportState, BBox } from './viewport.js';\r\nimport type { LayoutResult } from './layout.js';\r\nimport { englishTranslator } from '../i18n/translator.js';\r\nimport type { Translator } from '../i18n/types.js';\r\n\r\nconst SVG_NS = 'http://www.w3.org/2000/svg';\r\n\r\nexport interface MinimapOptions {\r\n /** Element the minimap SVG is appended into. Cleared on mount. */\r\n container: HTMLElement;\r\n /** Layout result whose shapes are rendered as silhouette rects. */\r\n layout: LayoutResult;\r\n /** Viewport to observe and control. */\r\n viewport: Viewport;\r\n /**\r\n * Main editor container - used to compute the current-view rectangle\r\n * (`containerSize / zoom` gives the scene span the user can see).\r\n */\r\n editorContainer: HTMLElement;\r\n /** Minimap width in CSS pixels. Default 200. */\r\n width?: number;\r\n /** Minimap height in CSS pixels. Default 150. */\r\n height?: number;\r\n /**\r\n * Optional lookup so silhouettes can be coloured by element type\r\n * (start events green, end events red, matching the canvas\r\n * renderers). Returns undefined if the type can't be resolved —\r\n * silhouette falls back to the neutral gray fill.\r\n */\r\n getElementType?: (elementId: string) => string | undefined;\r\n /**\r\n * Translator for the minimap's aria-label. Defaults to the built-in\r\n * English translator when omitted (standalone tests, callers that\r\n * never set a language).\r\n */\r\n translate?: Translator;\r\n}\r\n\r\nexport interface MinimapInstance {\r\n /** Re-render with the current viewport and layout state. */\r\n update(): void;\r\n /** Remove DOM and detach listeners. */\r\n dispose(): void;\r\n}\r\n\r\n/**\r\n * Mounts a minimap into `options.container`. Returns handles to\r\n * refresh or tear down the minimap. Safe to call with a disposed\r\n * viewport - `update()` becomes a no-op.\r\n */\r\nexport function mountMinimap(options: MinimapOptions): MinimapInstance {\r\n const t: Translator = options.translate ?? englishTranslator;\r\n const width = options.width ?? 200;\r\n const height = options.height ?? 150;\r\n\r\n const doc = options.container.ownerDocument;\r\n\r\n // Compute content bbox (union of every shape's bounds). Edges are\r\n // ignored - their waypoints are covered by the shapes they connect.\r\n const contentBox = computeContentBox(options.layout);\r\n\r\n // Root SVG.\r\n while (options.container.firstChild) {\r\n options.container.removeChild(options.container.firstChild);\r\n }\r\n const svg = doc.createElementNS(SVG_NS, 'svg') as SVGSVGElement;\r\n svg.setAttribute('class', 'mz-minimap');\r\n svg.setAttribute('width', String(width));\r\n svg.setAttribute('height', String(height));\r\n svg.setAttribute('role', 'img');\r\n svg.setAttribute('aria-label', t('minimap.aria'));\r\n svg.style.display = 'block';\r\n svg.style.background = 'var(--mz-color-surface-muted, #f3f4f6)';\r\n // No CSS border on the SVG. The viewport rect provides the only\r\n // outline the user sees inside the minimap; an extra decorative\r\n // border just inside the SVG edge would read as a second concentric\r\n // frame, especially when the viewport rect is clamped to fill the\r\n // drawable area at heavy zoom-out.\r\n svg.style.cursor = 'pointer';\r\n options.container.appendChild(svg);\r\n\r\n // Scale so contentBox fits inside (width, height) with 4-px padding.\r\n const pad = 4;\r\n const scale = Math.min(\r\n (width - pad * 2) / Math.max(1, contentBox.width),\r\n (height - pad * 2) / Math.max(1, contentBox.height),\r\n );\r\n const offsetX = pad + (width - pad * 2 - contentBox.width * scale) / 2;\r\n const offsetY = pad + (height - pad * 2 - contentBox.height * scale) / 2;\r\n\r\n const sceneToMini = (sx: number, sy: number): { x: number; y: number } => ({\r\n x: offsetX + (sx - contentBox.x) * scale,\r\n y: offsetY + (sy - contentBox.y) * scale,\r\n });\r\n\r\n // Shape silhouettes. Start / end events get the same accent colours\r\n // as the canvas renderer so the minimap reads as a true overview.\r\n for (const [id, s] of options.layout.shapes) {\r\n const p = sceneToMini(s.bounds.x, s.bounds.y);\r\n const rect = doc.createElementNS(SVG_NS, 'rect');\r\n rect.setAttribute('x', String(p.x));\r\n rect.setAttribute('y', String(p.y));\r\n rect.setAttribute('width', String(Math.max(1, s.bounds.width * scale)));\r\n rect.setAttribute('height', String(Math.max(1, s.bounds.height * scale)));\r\n const type = options.getElementType?.(id);\r\n if (type === 'startEvent') {\r\n rect.setAttribute('fill', 'var(--mz-color-event-start, #10b981)');\r\n rect.setAttribute('fill-opacity', '0.7');\r\n } else if (type === 'endEvent') {\r\n rect.setAttribute('fill', 'var(--mz-color-event-end, #ef4444)');\r\n rect.setAttribute('fill-opacity', '0.7');\r\n } else {\r\n rect.setAttribute('fill', 'var(--mz-color-stroke-default, #1f2937)');\r\n rect.setAttribute('fill-opacity', '0.35');\r\n }\r\n rect.setAttribute('stroke', 'none');\r\n svg.appendChild(rect);\r\n }\r\n\r\n // Viewport-rect overlay - updated on viewport changes.\r\n const vpRect = doc.createElementNS(SVG_NS, 'rect');\r\n vpRect.setAttribute('class', 'mz-minimap-viewport');\r\n vpRect.setAttribute('fill', 'var(--mz-color-selected-stroke, #2563eb)');\r\n vpRect.setAttribute('fill-opacity', '0.12');\r\n vpRect.setAttribute('stroke', 'var(--mz-color-selected-stroke, #2563eb)');\r\n vpRect.setAttribute('stroke-width', '1.5');\r\n vpRect.setAttribute('pointer-events', 'none');\r\n svg.appendChild(vpRect);\r\n\r\n function updateViewportRect(state: ViewportState): void {\r\n const zoom = state.zoom || 1;\r\n const rect = options.editorContainer.getBoundingClientRect();\r\n // Visible scene span is the container size divided by zoom.\r\n const visibleSceneW = rect.width / zoom;\r\n const visibleSceneH = rect.height / zoom;\r\n // Top-left of the visible scene region is -pan/zoom.\r\n const sceneX = -state.pan.x / zoom;\r\n const sceneY = -state.pan.y / zoom;\r\n const tl = sceneToMini(sceneX, sceneY);\r\n // Clamp the viewport rect to the minimap's drawable area so the\r\n // user always sees a single rectangle inside the minimap. Without\r\n // clamping, when the user can see more than the diagram bbox\r\n // (heavy zoom-out, or pan past the edge), the rect overflows the\r\n // SVG and the browser clips it — which visually reads as a\r\n // duplicate \"second border\" right inside the minimap frame.\r\n const drawX0 = pad;\r\n const drawY0 = pad;\r\n const drawX1 = width - pad;\r\n const drawY1 = height - pad;\r\n let x = tl.x;\r\n let y = tl.y;\r\n let w = visibleSceneW * scale;\r\n let h = visibleSceneH * scale;\r\n if (x < drawX0) {\r\n w -= drawX0 - x;\r\n x = drawX0;\r\n }\r\n if (x + w > drawX1) w = drawX1 - x;\r\n if (y < drawY0) {\r\n h -= drawY0 - y;\r\n y = drawY0;\r\n }\r\n if (y + h > drawY1) h = drawY1 - y;\r\n vpRect.setAttribute('x', String(x));\r\n vpRect.setAttribute('y', String(y));\r\n vpRect.setAttribute('width', String(Math.max(1, w)));\r\n vpRect.setAttribute('height', String(Math.max(1, h)));\r\n }\r\n\r\n updateViewportRect(options.viewport.getState());\r\n const unsubViewport = options.viewport.onChange(updateViewportRect);\r\n\r\n // Click-to-pan. Translate the clicked minimap point back into scene\r\n // coords, then compute the pan that centres that point in the editor\r\n // container.\r\n function onClick(ev: MouseEvent): void {\r\n const svgRect = svg.getBoundingClientRect();\r\n const mx = ev.clientX - svgRect.left;\r\n const my = ev.clientY - svgRect.top;\r\n const sceneX = contentBox.x + (mx - offsetX) / scale;\r\n const sceneY = contentBox.y + (my - offsetY) / scale;\r\n const state = options.viewport.getState();\r\n const editorRect = options.editorContainer.getBoundingClientRect();\r\n const zoom = state.zoom || 1;\r\n const newPanX = editorRect.width / 2 - sceneX * zoom;\r\n const newPanY = editorRect.height / 2 - sceneY * zoom;\r\n options.viewport.panTo(newPanX, newPanY);\r\n }\r\n svg.addEventListener('click', onClick);\r\n\r\n return {\r\n update(): void {\r\n updateViewportRect(options.viewport.getState());\r\n },\r\n dispose(): void {\r\n svg.removeEventListener('click', onClick);\r\n unsubViewport();\r\n if (svg.parentNode === options.container) {\r\n options.container.removeChild(svg);\r\n }\r\n },\r\n };\r\n}\r\n\r\nfunction computeContentBox(layout: LayoutResult): BBox {\r\n let minX = Number.POSITIVE_INFINITY;\r\n let minY = Number.POSITIVE_INFINITY;\r\n let maxX = Number.NEGATIVE_INFINITY;\r\n let maxY = Number.NEGATIVE_INFINITY;\r\n let seen = false;\r\n for (const [, s] of layout.shapes) {\r\n const b = s.bounds;\r\n if (b.x < minX) minX = b.x;\r\n if (b.y < minY) minY = b.y;\r\n if (b.x + b.width > maxX) maxX = b.x + b.width;\r\n if (b.y + b.height > maxY) maxY = b.y + b.height;\r\n seen = true;\r\n }\r\n if (!seen) {\r\n return { x: 0, y: 0, width: 100, height: 100 };\r\n }\r\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - quick-pattern gallery\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Small in-canvas picker for common BPMN snippets. It turns the public\r\n * createQuickPattern API into a discoverable authoring affordance without\r\n * requiring host applications to build their own menu first.\r\n */\r\n\r\nimport type { BpmnQuickPattern } from '../api/BpmnEditor.js';\r\nimport { englishTranslator } from '../i18n/translator.js';\r\nimport type { Translator } from '../i18n/types.js';\r\n\r\nexport interface QuickPatternDefinition {\r\n id: BpmnQuickPattern;\r\n label: string;\r\n description: string;\r\n}\r\n\r\nexport const QUICK_PATTERN_DEFINITIONS: readonly QuickPatternDefinition[] = [\r\n {\r\n id: 'approval',\r\n label: 'Approval flow',\r\n description: 'User task followed by a decision point.',\r\n },\r\n {\r\n id: 'decision',\r\n label: 'Decision branch',\r\n description: 'Exclusive gateway with two outgoing task branches.',\r\n },\r\n {\r\n id: 'parallelSplitJoin',\r\n label: 'Parallel split/join',\r\n description: 'Parallel gateway, two branches, and a join gateway.',\r\n },\r\n {\r\n id: 'exceptionPath',\r\n label: 'Exception path',\r\n description: 'Task followed by an exception event path.',\r\n },\r\n {\r\n id: 'timerEscalation',\r\n label: 'Timer escalation',\r\n description: 'Task followed by an intermediate timer event.',\r\n },\r\n];\r\n\r\nexport interface QuickPatternGalleryOptions {\r\n container: HTMLElement;\r\n anchorRect: { left: number; top: number; right: number; bottom: number };\r\n onPick: (pattern: BpmnQuickPattern) => void;\r\n onDismiss?: () => void;\r\n /**\r\n * Translator for the gallery title / aria text and per-pattern\r\n * label + description. Defaults to the built-in English translator\r\n * when omitted (standalone tests, callers that never set a language).\r\n */\r\n translate?: Translator;\r\n}\r\n\r\nexport interface QuickPatternGalleryHandle {\r\n close: () => void;\r\n}\r\n\r\nexport function openQuickPatternGallery(\r\n options: QuickPatternGalleryOptions,\r\n): QuickPatternGalleryHandle {\r\n const doc = options.container.ownerDocument;\r\n const t: Translator = options.translate ?? englishTranslator;\r\n injectStyles(doc);\r\n\r\n const root = doc.createElement('div');\r\n root.className = 'mz-quick-pattern-gallery';\r\n root.setAttribute('data-mz-chrome', 'true');\r\n root.setAttribute('data-testid', 'quick-pattern-gallery');\r\n root.setAttribute('role', 'menu');\r\n root.setAttribute('aria-label', t('quickPattern.aria.menu'));\r\n\r\n const title = doc.createElement('div');\r\n title.className = 'mz-quick-pattern-gallery-title';\r\n title.setAttribute('data-mz-chrome', 'true');\r\n title.textContent = t('quickPattern.title');\r\n root.appendChild(title);\r\n\r\n for (const pattern of QUICK_PATTERN_DEFINITIONS) {\r\n const button = doc.createElement('button');\r\n button.type = 'button';\r\n button.className = 'mz-quick-pattern-gallery-item';\r\n button.setAttribute('data-mz-chrome', 'true');\r\n button.setAttribute('data-testid', `quick-pattern-${pattern.id}`);\r\n button.setAttribute('role', 'menuitem');\r\n button.innerHTML =\r\n `${escapeHtml(t(`quickPattern.${pattern.id}.label`))}` +\r\n `${escapeHtml(t(`quickPattern.${pattern.id}.desc`))}`;\r\n button.addEventListener('click', (ev) => {\r\n ev.preventDefault();\r\n ev.stopPropagation();\r\n close();\r\n options.onPick(pattern.id);\r\n });\r\n button.addEventListener('pointerdown', (ev) => {\r\n ev.stopPropagation();\r\n });\r\n root.appendChild(button);\r\n }\r\n\r\n position(root, options.anchorRect, options.container);\r\n options.container.appendChild(root);\r\n position(root, options.anchorRect, options.container);\r\n\r\n let closed = false;\r\n function close(): void {\r\n if (closed) return;\r\n closed = true;\r\n doc.removeEventListener('mousedown', onDocumentMouseDown, true);\r\n doc.removeEventListener('keydown', onDocumentKeyDown, true);\r\n if (root.parentNode === options.container) {\r\n options.container.removeChild(root);\r\n }\r\n }\r\n\r\n function dismiss(): void {\r\n close();\r\n options.onDismiss?.();\r\n }\r\n\r\n function onDocumentMouseDown(ev: MouseEvent): void {\r\n if (!root.contains(ev.target as Node | null)) {\r\n dismiss();\r\n }\r\n }\r\n\r\n function onDocumentKeyDown(ev: KeyboardEvent): void {\r\n if (ev.key === 'Escape') {\r\n ev.preventDefault();\r\n dismiss();\r\n }\r\n }\r\n\r\n doc.addEventListener('mousedown', onDocumentMouseDown, true);\r\n doc.addEventListener('keydown', onDocumentKeyDown, true);\r\n\r\n return { close };\r\n}\r\n\r\nfunction position(\r\n root: HTMLElement,\r\n anchorRect: { left: number; top: number; right: number; bottom: number },\r\n container: HTMLElement,\r\n): void {\r\n const containerRect = container.getBoundingClientRect();\r\n const left = Math.max(8, anchorRect.left - containerRect.left - 250);\r\n const top = Math.max(8, anchorRect.bottom - containerRect.top + 8);\r\n root.style.left = `${String(left)}px`;\r\n root.style.top = `${String(top)}px`;\r\n}\r\n\r\nlet stylesInjected = false;\r\n\r\nfunction injectStyles(doc: Document): void {\r\n if (stylesInjected) return;\r\n stylesInjected = true;\r\n const style = doc.createElement('style');\r\n style.textContent = `\r\n.mz-quick-pattern-gallery {\r\n position: absolute;\r\n width: 280px;\r\n max-height: 360px;\r\n overflow: auto;\r\n z-index: 70;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 4px;\r\n padding: 8px;\r\n background: var(--mz-color-surface-default, rgb(var(--background, 255 255 255)));\r\n border: 1px solid var(--mz-color-stroke-muted, rgb(var(--headertext-inactive, 127 151 164)));\r\n border-radius: 8px;\r\n box-shadow: 0 12px 28px var(--mz-color-shadow-strong, rgba(15, 23, 42, 0.22));\r\n color: var(--mz-color-stroke-strong, rgb(var(--headertext, 0 47 73)));\r\n font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\r\n}\r\n.mz-quick-pattern-gallery-title {\r\n padding: 4px 6px 8px;\r\n font-size: 12px;\r\n font-weight: 700;\r\n color: var(--mz-color-stroke-default, rgb(var(--bodytext, 0 47 73)));\r\n text-transform: uppercase;\r\n letter-spacing: 0.04em;\r\n}\r\n.mz-quick-pattern-gallery-item {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n padding: 8px;\r\n border: 1px solid transparent;\r\n border-radius: 6px;\r\n background: transparent;\r\n color: inherit;\r\n text-align: left;\r\n cursor: pointer;\r\n}\r\n.mz-quick-pattern-gallery-item:hover,\r\n.mz-quick-pattern-gallery-item:focus-visible {\r\n background: var(--mz-color-surface-muted, rgba(var(--containerwrapper-alternate, 238 241 250), 0.9));\r\n border-color: var(--mz-color-accent-muted, rgba(var(--primary, 134 118 255), 0.3));\r\n outline: none;\r\n}\r\n.mz-quick-pattern-gallery-label {\r\n font-size: 13px;\r\n font-weight: 650;\r\n}\r\n.mz-quick-pattern-gallery-description {\r\n font-size: 12px;\r\n color: var(--mz-color-stroke-muted, rgb(var(--headertext-inactive, 127 151 164)));\r\n}\r\n`;\r\n doc.head.appendChild(style);\r\n}\r\n\r\nfunction escapeHtml(value: string): string {\r\n return value\r\n .replace(/&/g, '&')\r\n .replace(//g, '>')\r\n .replace(/\"/g, '"');\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - lightweight report panel\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Reusable in-canvas text panel for validation and route-quality reports.\r\n */\r\n\r\nimport { englishTranslator } from '../i18n/translator.js';\r\nimport type { Translator } from '../i18n/types.js';\r\n\r\nexport interface ReportPanelOptions {\r\n container: HTMLElement;\r\n title: string;\r\n body: string;\r\n testId: string;\r\n /**\r\n * Translator for the close-button aria-label. Defaults to the\r\n * built-in English translator when omitted (standalone tests, callers\r\n * that never set a language). The panel title is caller-supplied and\r\n * is not translated here.\r\n */\r\n translate?: Translator;\r\n}\r\n\r\nexport interface ReportPanelHandle {\r\n close: () => void;\r\n}\r\n\r\nexport function openReportPanel(options: ReportPanelOptions): ReportPanelHandle {\r\n const doc = options.container.ownerDocument;\r\n const t: Translator = options.translate ?? englishTranslator;\r\n injectStyles(doc);\r\n\r\n const root = doc.createElement('section');\r\n root.className = 'mz-report-panel';\r\n root.setAttribute('data-mz-chrome', 'true');\r\n root.setAttribute('data-testid', options.testId);\r\n root.setAttribute('role', 'dialog');\r\n root.setAttribute('aria-label', options.title);\r\n\r\n // Stop pointer events from bubbling to the editor's viewport so the\r\n // pan-drag handler never claims a click landing on this panel. Same fix\r\n // the toolbar and palette use (see controls.ts / palette.ts\r\n // onPointerDown). Without it the viewport takes a pointer capture on the\r\n // CONTAINER, which retargets the pointerup away from the close button;\r\n // the browser then fires `click` on their common ancestor instead of the\r\n // button, its handler never runs, and the panel could only be dismissed\r\n // by reloading the page (#8158). It also keeps a text selection drag in\r\n // the report body from panning the canvas underneath.\r\n const onPointerDown = (ev: Event): void => {\r\n ev.stopPropagation();\r\n };\r\n root.addEventListener('pointerdown', onPointerDown);\r\n root.addEventListener('mousedown', onPointerDown);\r\n\r\n const header = doc.createElement('div');\r\n header.className = 'mz-report-panel-header';\r\n header.setAttribute('data-mz-chrome', 'true');\r\n\r\n const title = doc.createElement('strong');\r\n title.textContent = options.title;\r\n header.appendChild(title);\r\n\r\n const closeButton = doc.createElement('button');\r\n closeButton.type = 'button';\r\n closeButton.className = 'mz-report-panel-close';\r\n closeButton.setAttribute('data-mz-chrome', 'true');\r\n closeButton.setAttribute('aria-label', t('reportPanel.close.aria'));\r\n closeButton.textContent = 'x';\r\n closeButton.addEventListener('click', (ev) => {\r\n ev.preventDefault();\r\n ev.stopPropagation();\r\n close();\r\n });\r\n header.appendChild(closeButton);\r\n root.appendChild(header);\r\n\r\n const pre = doc.createElement('pre');\r\n pre.className = 'mz-report-panel-body';\r\n pre.setAttribute('data-mz-chrome', 'true');\r\n pre.textContent = options.body;\r\n root.appendChild(pre);\r\n\r\n ensureContainerPositioned(options.container);\r\n options.container.appendChild(root);\r\n\r\n let closed = false;\r\n function close(): void {\r\n if (closed) return;\r\n closed = true;\r\n root.removeEventListener('pointerdown', onPointerDown);\r\n root.removeEventListener('mousedown', onPointerDown);\r\n if (root.parentNode === options.container) {\r\n options.container.removeChild(root);\r\n }\r\n }\r\n\r\n return { close };\r\n}\r\n\r\nfunction ensureContainerPositioned(container: HTMLElement): void {\r\n const cs = (\r\n container.ownerDocument?.defaultView ?? globalThis\r\n ).getComputedStyle?.(container);\r\n if (cs && cs.position === 'static') {\r\n container.style.position = 'relative';\r\n }\r\n}\r\n\r\nlet stylesInjected = false;\r\n\r\nfunction injectStyles(doc: Document): void {\r\n if (stylesInjected) return;\r\n stylesInjected = true;\r\n const style = doc.createElement('style');\r\n style.textContent = `\r\n.mz-report-panel {\r\n position: absolute;\r\n right: 12px;\r\n bottom: 12px;\r\n width: min(440px, calc(100% - 24px));\r\n max-height: min(480px, calc(100% - 24px));\r\n z-index: 65;\r\n display: flex;\r\n flex-direction: column;\r\n background: var(--mz-color-surface-default, rgb(var(--background, 255 255 255)));\r\n border: 1px solid var(--mz-color-stroke-muted, rgb(var(--headertext-inactive, 127 151 164)));\r\n border-radius: 10px;\r\n box-shadow: 0 12px 28px var(--mz-color-shadow-strong, rgba(15, 23, 42, 0.22));\r\n color: var(--mz-color-stroke-strong, rgb(var(--headertext, 0 47 73)));\r\n font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\r\n}\r\n.mz-report-panel-header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 12px;\r\n padding: 10px 12px;\r\n border-bottom: 1px solid var(--mz-color-stroke-subtle, rgba(var(--headertext, 0 47 73), 0.16));\r\n}\r\n.mz-report-panel-close {\r\n width: 24px;\r\n height: 24px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border: 1px solid transparent;\r\n border-radius: 5px;\r\n background: transparent;\r\n color: var(--mz-color-stroke-default, rgb(var(--bodytext, 0 47 73)));\r\n cursor: pointer;\r\n font-size: 18px;\r\n line-height: 1;\r\n}\r\n.mz-report-panel-close:hover,\r\n.mz-report-panel-close:focus-visible {\r\n background: var(--mz-color-surface-muted, rgba(var(--containerwrapper, 212 219 242), 0.7));\r\n outline: none;\r\n}\r\n.mz-report-panel-body {\r\n margin: 0;\r\n padding: 12px;\r\n overflow: auto;\r\n white-space: pre-wrap;\r\n font-size: 12px;\r\n line-height: 1.45;\r\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\r\n}\r\n`;\r\n doc.head.appendChild(style);\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - palette icon glyphs\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: small inline-SVG factories used by the palette / toolbox\r\n * sidebar (view/palette.ts). One factory per CreateShapeType supported\r\n * by BpmnEditor.createShape.\r\n *\r\n * Most icons stay monochrome — they use `currentColor` so the host\r\n * can theme the palette via a single CSS color rule. Start and End\r\n * events are the exception: they use the same `--mz-color-event-start`\r\n * / `--mz-color-event-end` tokens as the canvas renderer so the palette\r\n * reads consistently with the diagram (green start, red end).\r\n *\r\n * Pure: returns SVG markup as strings; no DOM access.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { CreateShapeType } from '../api/shape-types.js';\r\n\r\n/**\r\n * Display name shown in tooltip + aria-label for each palette item.\r\n * Keep terse — these are toolbar tooltips, not full descriptions.\r\n */\r\n/**\r\n * Palette category for grouping headers in wide mode. Mechanical\r\n * mapping: anything ending in `Event` is Events, anything ending in\r\n * `Task` plus `callActivity` is Tasks, anything ending in `Gateway`\r\n * is Gateways, the remainder is Containers (sub-process, data\r\n * object/store, text annotation).\r\n */\r\nexport type PaletteCategory = 'Events' | 'Tasks' | 'Gateways' | 'Containers';\r\n\r\nexport function paletteCategoryFor(type: CreateShapeType): PaletteCategory {\r\n // Case-insensitive endsWith — `task` (lower) and `userTask` (camel)\r\n // both end with the same suffix when normalised.\r\n const lower = type.toLowerCase();\r\n if (lower.endsWith('event')) return 'Events';\r\n if (lower.endsWith('task') || type === 'callActivity') return 'Tasks';\r\n if (lower.endsWith('gateway')) return 'Gateways';\r\n return 'Containers';\r\n}\r\n\r\nexport const SHAPE_DISPLAY_NAMES: Readonly> = {\r\n // Events\r\n startEvent: 'Start event',\r\n endEvent: 'End event',\r\n intermediateThrowEvent: 'Intermediate throw event',\r\n intermediateCatchEvent: 'Intermediate catch event',\r\n // Tasks\r\n task: 'Task',\r\n userTask: 'User task',\r\n serviceTask: 'Service task',\r\n sendTask: 'Send task',\r\n receiveTask: 'Receive task',\r\n manualTask: 'Manual task',\r\n businessRuleTask: 'Business rule task',\r\n scriptTask: 'Script task',\r\n callActivity: 'Call activity',\r\n // Gateways\r\n exclusiveGateway: 'Exclusive gateway',\r\n parallelGateway: 'Parallel gateway',\r\n inclusiveGateway: 'Inclusive gateway',\r\n eventBasedGateway: 'Event-based gateway',\r\n complexGateway: 'Complex gateway',\r\n // Containers / data / artifacts\r\n subProcess: 'Sub-process',\r\n adHocSubProcess: 'Ad-hoc sub-process',\r\n transaction: 'Transaction',\r\n dataObject: 'Data object',\r\n dataObjectReference: 'Data object reference',\r\n dataStoreReference: 'Data store',\r\n textAnnotation: 'Text annotation',\r\n group: 'Group',\r\n};\r\n\r\n/**\r\n * Returns an SVG markup string sized to fit a 24x24 button glyph.\r\n * Most strokes use `currentColor` so the host can theme the palette\r\n * via a single CSS color rule. Start / End events use the matching\r\n * canvas token so the palette and the diagram read together.\r\n */\r\nexport function paletteIconFor(type: CreateShapeType): string {\r\n switch (type) {\r\n /* ----- Events ----- */\r\n case 'startEvent':\r\n return svg(`\r\n \r\n `);\r\n case 'endEvent':\r\n return svg(`\r\n \r\n `);\r\n case 'intermediateThrowEvent':\r\n // Two concentric rings, solid filled inner glyph (throw = filled marker).\r\n return svg(`\r\n \r\n \r\n `);\r\n case 'intermediateCatchEvent':\r\n // Two concentric rings, smaller second one (catch = hollow marker).\r\n return svg(`\r\n \r\n \r\n `);\r\n\r\n /* ----- Tasks (all share rounded-rect outline + a glyph) ----- */\r\n case 'task':\r\n return svg(`\r\n \r\n `);\r\n case 'userTask':\r\n return svg(`\r\n \r\n \r\n \r\n `);\r\n case 'serviceTask':\r\n return svg(`\r\n \r\n \r\n `);\r\n case 'sendTask':\r\n // Filled-envelope glyph = \"send\".\r\n return svg(`\r\n \r\n \r\n \r\n `);\r\n case 'receiveTask':\r\n // Outline-envelope glyph = \"receive\".\r\n return svg(`\r\n \r\n \r\n \r\n `);\r\n case 'manualTask':\r\n // Hand silhouette = manual.\r\n return svg(`\r\n \r\n \r\n `);\r\n case 'businessRuleTask':\r\n // Stack of horizontal bars = rule table.\r\n return svg(`\r\n \r\n \r\n \r\n `);\r\n case 'scriptTask':\r\n // Document-with-curl = script.\r\n return svg(`\r\n \r\n \r\n \r\n `);\r\n case 'callActivity':\r\n // Thick outline = call activity.\r\n return svg(`\r\n \r\n `);\r\n\r\n /* ----- Gateways (all diamond + interior marker) ----- */\r\n case 'exclusiveGateway':\r\n return svg(`\r\n \r\n \r\n `);\r\n case 'parallelGateway':\r\n return svg(`\r\n \r\n \r\n `);\r\n case 'inclusiveGateway':\r\n // Diamond + circle (O marker).\r\n return svg(`\r\n \r\n \r\n `);\r\n case 'eventBasedGateway':\r\n // Diamond + pentagon-ish marker.\r\n return svg(`\r\n \r\n \r\n \r\n `);\r\n case 'complexGateway':\r\n // Diamond + asterisk marker.\r\n return svg(`\r\n \r\n \r\n `);\r\n\r\n /* ----- Containers / data / artifacts ----- */\r\n case 'subProcess':\r\n // Rounded rect with [+] marker bottom-center.\r\n return svg(`\r\n \r\n \r\n \r\n `);\r\n case 'dataObject':\r\n // Page with folded corner.\r\n return svg(`\r\n \r\n \r\n `);\r\n case 'dataStoreReference':\r\n // Cylinder.\r\n return svg(`\r\n \r\n \r\n \r\n \r\n `);\r\n case 'textAnnotation':\r\n // Bracket + horizontal lines (note glyph).\r\n return svg(`\r\n \r\n \r\n `);\r\n case 'adHocSubProcess':\r\n // Rounded rect with a tilde (~) marker bottom-center = ad-hoc.\r\n return svg(`\r\n \r\n \r\n `);\r\n case 'transaction':\r\n // Double-border rounded rect = transaction.\r\n return svg(`\r\n \r\n \r\n `);\r\n case 'dataObjectReference':\r\n // Page with folded corner — deliberately identical to the\r\n // `dataObject` glyph above (#8345). BPMN 2.0 defines no reference\r\n // arrow on a data object reference: the only markers the spec puts\r\n // on the page shape are the collection bars and the DataInput /\r\n // DataOutput arrows, and neither applies here. This icon used to\r\n // draw a fabricated \"reference arrow\" that the canvas renderer\r\n // (view/renderers/data.ts renderDataObjectReference) correctly\r\n // never drew, so the palette promised a visual the diagram could\r\n // not deliver. The palette is the side that was wrong.\r\n //\r\n // The two entries are told apart in the flyout by their name and\r\n // description line (view/palette.ts VARIANT_DESCRIPTION_KEYS), not\r\n // by a glyph BPMN does not have.\r\n return svg(`\r\n \r\n \r\n `);\r\n case 'group':\r\n // Dashed rounded rect = group (artifact).\r\n return svg(`\r\n \r\n `);\r\n }\r\n}\r\n\r\nfunction svg(inner: string): string {\r\n return (\r\n `` +\r\n inner.trim() +\r\n ``\r\n );\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - palette drag gesture\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: pointer-based drag-to-create from the palette (Phase 11a/b).\r\n * Pointerdown on a palette button starts a ghost preview that tracks the\r\n * cursor; on drop the gesture dispatches one of three outcomes:\r\n *\r\n * 1. Drop on empty canvas\r\n * -> create the shape at the cursor's scene coordinates.\r\n * 2. Drop on top of an existing eligible shape\r\n * -> append the new shape after the target (facade adds the\r\n * sequenceFlow).\r\n * 3. Pointer never moved past `DRAG_THRESHOLD_PX`\r\n * -> the gesture is treated as a click; this module no-ops and\r\n * the existing palette click handler runs the auto-place path.\r\n *\r\n * Uses pointer events end-to-end (no HTML5 native drag-and-drop) so the\r\n * gesture matches `shape-drag.ts` and `connect-drag.ts` in behaviour and\r\n * cancellation semantics. The ghost preview is appended to the canvas\r\n * container so it inherits the same coordinate space as the SVG host.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { CreateShapeType } from '../api/shape-types.js';\r\nimport type { Viewport } from './viewport.js';\r\nimport { paletteIconFor } from './palette-icons.js';\r\n\r\n/** Pixel distance the pointer must move before a click upgrades to drag. */\r\nconst DRAG_THRESHOLD_PX = 4;\r\n\r\n/** Element types we accept as drop-on-shape targets (append-after). */\r\nconst APPEND_ELIGIBLE_TYPES: ReadonlySet = new Set([\r\n 'task',\r\n 'userTask',\r\n 'serviceTask',\r\n 'sendTask',\r\n 'receiveTask',\r\n 'manualTask',\r\n 'businessRuleTask',\r\n 'scriptTask',\r\n 'callActivity',\r\n 'startEvent',\r\n 'intermediateCatchEvent',\r\n 'intermediateThrowEvent',\r\n 'exclusiveGateway',\r\n 'parallelGateway',\r\n 'inclusiveGateway',\r\n 'eventBasedGateway',\r\n 'complexGateway',\r\n 'subProcess',\r\n]);\r\n\r\n/** Scene-space drop payload dispatched by the gesture on a successful drop. */\r\nexport interface PaletteDropPayload {\r\n /** The CreateShapeType picked from the palette. */\r\n type: CreateShapeType;\r\n /** Scene-space drop coordinates (only when dropping on empty canvas). */\r\n sceneX?: number;\r\n /** Scene-space drop coordinates (only when dropping on empty canvas). */\r\n sceneY?: number;\r\n /**\r\n * When set, the drop landed on an existing shape and the facade should\r\n * append the new shape after this source id (i.e. create-and-connect).\r\n */\r\n appendSourceId?: string;\r\n /**\r\n * Phase 11c: when set, the drop landed on an existing sequence flow\r\n * and the facade should split-and-insert (`A -> X -> B`). Mutually\r\n * exclusive with `appendSourceId` and scene coords.\r\n */\r\n splitEdgeId?: string;\r\n}\r\n\r\nexport interface PaletteDragOptions {\r\n /** The palette root element (returned by `attachPalette`). */\r\n paletteRoot: HTMLElement;\r\n /** The editor host container (parent of the canvas SVG). */\r\n container: HTMLElement;\r\n /**\r\n * The live viewport controller (or null while nothing is mounted).\r\n * The gesture needs the controller — not a state snapshot — because\r\n * pointer-to-scene conversion is anchored to the SVG canvas origin,\r\n * which only the viewport knows (#7862).\r\n */\r\n getViewport: () => Viewport | null;\r\n /** Invoked on successful drop. */\r\n onDrop: (payload: PaletteDropPayload) => void;\r\n}\r\n\r\n/**\r\n * Installs the palette drag-to-create gesture. Returns a teardown that\r\n * removes every listener and clears any in-flight preview.\r\n */\r\nexport function attachPaletteDrag(options: PaletteDragOptions): () => void {\r\n const { paletteRoot, container, getViewport, onDrop } = options;\r\n const doc = paletteRoot.ownerDocument;\r\n\r\n interface DragState {\r\n type: CreateShapeType;\r\n pointerId: number;\r\n startClient: { x: number; y: number };\r\n ghost: HTMLElement | null;\r\n moved: boolean;\r\n /** Element currently highlighted as an append target, if any. */\r\n highlighted: Element | null;\r\n /** The palette button the drag originated from. */\r\n sourceButton: HTMLButtonElement;\r\n }\r\n let state: DragState | null = null;\r\n\r\n // We capture pointerdown on the palette root in bubble phase so the\r\n // palette's own pointerdown handler (which stops propagation for\r\n // pointer-capture reasons) does not run first - both can coexist\r\n // because they only inspect the event, never consume it.\r\n function onPointerDown(ev: PointerEvent): void {\r\n if (ev.button !== 0) return;\r\n const target = ev.target as Element | null;\r\n if (!target) return;\r\n const button = target.closest('button.mz-palette-item, button.mz-palette-variant');\r\n if (!button) return;\r\n // The toggle chevron is a button too - skip it.\r\n if (button.classList.contains('mz-palette-toggle')) return;\r\n const type = button.getAttribute('data-shape-type') as CreateShapeType | null;\r\n if (!type) return;\r\n\r\n state = {\r\n type,\r\n pointerId: ev.pointerId,\r\n startClient: { x: ev.clientX, y: ev.clientY },\r\n ghost: null,\r\n moved: false,\r\n highlighted: null,\r\n sourceButton: button,\r\n };\r\n }\r\n\r\n function onPointerMove(ev: PointerEvent): void {\r\n if (!state || ev.pointerId !== state.pointerId) return;\r\n const dx = ev.clientX - state.startClient.x;\r\n const dy = ev.clientY - state.startClient.y;\r\n if (!state.moved) {\r\n if (Math.abs(dx) < DRAG_THRESHOLD_PX && Math.abs(dy) < DRAG_THRESHOLD_PX) return;\r\n state.moved = true;\r\n state.ghost = buildGhost(doc, state.type);\r\n container.appendChild(state.ghost);\r\n // The palette's button click would otherwise fire after pointerup\r\n // because pointerdown and pointerup share the same root target.\r\n // Install a one-shot capture-phase click suppressor so the click\r\n // never reaches the palette's onClick (which auto-places a shape\r\n // at the legacy default location).\r\n installClickSuppressor(paletteRoot);\r\n }\r\n if (state.ghost) {\r\n positionGhost(state.ghost, container, ev.clientX, ev.clientY);\r\n }\r\n updateAppendHighlight(state, ev);\r\n }\r\n\r\n function onPointerUp(ev: PointerEvent): void {\r\n if (!state || ev.pointerId !== state.pointerId) return;\r\n const current = state;\r\n state = null;\r\n if (current.highlighted) {\r\n clearHighlight(current.highlighted);\r\n }\r\n if (current.ghost && current.ghost.parentNode === container) {\r\n container.removeChild(current.ghost);\r\n }\r\n if (!current.moved) {\r\n // No drag happened - let the palette's click handler run normally.\r\n return;\r\n }\r\n // Pointer-up resolution order (most specific first):\r\n // 1. drop on shape -> append-after\r\n // 2. drop on edge -> split-and-insert (Phase 11c)\r\n // 3. drop on canvas -> place at cursor\r\n // We refind the target at release time so a brief out-and-back\r\n // hover still resolves correctly.\r\n const appendTargetId = findAppendTargetId(doc, ev.clientX, ev.clientY);\r\n if (appendTargetId) {\r\n onDrop({ type: current.type, appendSourceId: appendTargetId });\r\n return;\r\n }\r\n const splitEdgeId = findSplitEdgeId(doc, ev.clientX, ev.clientY);\r\n if (splitEdgeId) {\r\n onDrop({ type: current.type, splitEdgeId });\r\n return;\r\n }\r\n const scene = clientToScene(ev.clientX, ev.clientY, getViewport());\r\n if (!scene) return;\r\n onDrop({ type: current.type, sceneX: scene.x, sceneY: scene.y });\r\n }\r\n\r\n function onPointerCancel(ev: PointerEvent): void {\r\n if (!state || ev.pointerId !== state.pointerId) return;\r\n const current = state;\r\n state = null;\r\n if (current.highlighted) {\r\n clearHighlight(current.highlighted);\r\n }\r\n if (current.ghost && current.ghost.parentNode === container) {\r\n container.removeChild(current.ghost);\r\n }\r\n }\r\n\r\n function updateAppendHighlight(s: DragState, ev: PointerEvent): void {\r\n // Highlight whichever takes precedence at the pointer: shape > edge.\r\n let targetNode: Element | null = null;\r\n const appendTargetId = findAppendTargetId(doc, ev.clientX, ev.clientY);\r\n if (appendTargetId) {\r\n targetNode = doc.querySelector(`[data-element-id=\"${cssEscape(appendTargetId)}\"]`);\r\n } else {\r\n const edgeId = findSplitEdgeId(doc, ev.clientX, ev.clientY);\r\n if (edgeId) {\r\n targetNode = doc.querySelector(`[data-element-id=\"${cssEscape(edgeId)}\"]`);\r\n }\r\n }\r\n if (s.highlighted === targetNode) return;\r\n if (s.highlighted) {\r\n clearHighlight(s.highlighted);\r\n }\r\n s.highlighted = targetNode;\r\n if (targetNode) {\r\n addHighlight(targetNode);\r\n }\r\n }\r\n\r\n // Capture-phase pointermove/up on the document so the gesture keeps\r\n // tracking even when the cursor leaves the palette element.\r\n paletteRoot.addEventListener('pointerdown', onPointerDown);\r\n doc.addEventListener('pointermove', onPointerMove);\r\n doc.addEventListener('pointerup', onPointerUp);\r\n doc.addEventListener('pointercancel', onPointerCancel);\r\n\r\n return (): void => {\r\n paletteRoot.removeEventListener('pointerdown', onPointerDown);\r\n doc.removeEventListener('pointermove', onPointerMove);\r\n doc.removeEventListener('pointerup', onPointerUp);\r\n doc.removeEventListener('pointercancel', onPointerCancel);\r\n if (state?.highlighted) {\r\n clearHighlight(state.highlighted);\r\n }\r\n if (state?.ghost && state.ghost.parentNode === container) {\r\n container.removeChild(state.ghost);\r\n }\r\n state = null;\r\n };\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Helpers */\r\n/* ------------------------------------------------------------------ */\r\n\r\nexport function buildGhost(doc: Document, type: CreateShapeType): HTMLElement {\r\n const ghost = doc.createElement('div');\r\n ghost.className = 'mz-palette-ghost';\r\n ghost.setAttribute('data-mz-chrome', 'true');\r\n ghost.setAttribute('aria-hidden', 'true');\r\n const ps = ghost.style;\r\n ps.position = 'absolute';\r\n ps.pointerEvents = 'none';\r\n ps.zIndex = '50';\r\n ps.width = '48px';\r\n ps.height = '48px';\r\n ps.display = 'flex';\r\n ps.alignItems = 'center';\r\n ps.justifyContent = 'center';\r\n ps.background = 'rgba(99, 102, 241, 0.12)';\r\n ps.border = '1px dashed rgba(99, 102, 241, 0.7)';\r\n ps.borderRadius = '6px';\r\n ps.opacity = '0.85';\r\n ps.transform = 'translate(-50%, -50%)';\r\n ghost.innerHTML = paletteIconFor(type);\r\n // Keep the cloned icon from intercepting pointer events.\r\n const svg = ghost.querySelector('svg');\r\n if (svg) {\r\n svg.setAttribute('width', '32');\r\n svg.setAttribute('height', '32');\r\n (svg as SVGSVGElement).style.pointerEvents = 'none';\r\n }\r\n return ghost;\r\n}\r\n\r\nexport function positionGhost(\r\n ghost: HTMLElement,\r\n container: HTMLElement,\r\n clientX: number,\r\n clientY: number,\r\n): void {\r\n const rect = container.getBoundingClientRect();\r\n ghost.style.left = `${String(clientX - rect.left)}px`;\r\n ghost.style.top = `${String(clientY - rect.top)}px`;\r\n}\r\n\r\nexport function clientToScene(\r\n clientX: number,\r\n clientY: number,\r\n viewport: Viewport | null,\r\n): { x: number; y: number } | null {\r\n if (!viewport) return null;\r\n // Canonical pointer -> scene transform, anchored to the SVG canvas\r\n // origin. A container-based inversion here dropped shapes offset by\r\n // the palette width (#7862 family).\r\n return viewport.screenToScene(clientX, clientY);\r\n}\r\n\r\n/**\r\n * Element types whose EXPANDED form is a container: their body is the\r\n * canvas for the elements they hold, so a drop inside them means \"put\r\n * it in here\", never \"append after this activity\". Collapsed forms are\r\n * ordinary activity boxes and stay append-eligible - the renderer\r\n * distinguishes the two with `data-expanded` (#8185).\r\n */\r\nconst CONTAINER_WHEN_EXPANDED_TYPES: ReadonlySet = new Set([\r\n 'subProcess',\r\n 'adHocSubProcess',\r\n 'transaction',\r\n]);\r\n\r\n/**\r\n * True when the shape under the pointer is an EXPANDED container\r\n * (sub-process family). Such a hit is not an append target: the user\r\n * pointed at the inside of the container, and appending would place the\r\n * new element beside it instead - the #8185 symptom.\r\n */\r\nfunction isExpandedContainer(shape: Element): boolean {\r\n const type = shape.getAttribute('data-type') ?? '';\r\n if (!CONTAINER_WHEN_EXPANDED_TYPES.has(type)) return false;\r\n // Absent attribute = expanded: a sub-process DI without `isExpanded`\r\n // renders as the big open box, which is exactly the container case.\r\n return shape.getAttribute('data-expanded') !== 'false';\r\n}\r\n\r\n/**\r\n * Types whose BODY is a placement surface rather than an append target,\r\n * even though they are not containers and hold no children.\r\n *\r\n * A `callActivity` stands for an entire called process and is routinely\r\n * resized to enclose the context it calls, so a drop inside it means\r\n * \"put it here\", exactly as it does for an expanded sub-process body.\r\n * Classifying such a drop as append-after discarded the cursor position\r\n * and created the element 60px to the RIGHT of the box - outside the\r\n * region the user clicked in (#8233).\r\n *\r\n * Containment and cursor placement are SEPARATE decisions. The element\r\n * lands at the cursor AND remains a process sibling: a BPMN 2.0\r\n * `callActivity` owns no `flowElements`, so `findSubProcessContainerAtPoint`\r\n * still refuses to parent into it and the XML stays valid. The paint-order\r\n * twin of this set is `BACKDROP_ONLY_TYPES` in `pipeline.ts`.\r\n *\r\n * Appending after a call activity is still available, explicitly, from the\r\n * context pad - which is a deliberate instruction rather than an inference\r\n * drawn from where the pointer happened to be.\r\n */\r\nconst PLACEMENT_SURFACE_TYPES: ReadonlySet = new Set([\r\n 'callActivity',\r\n]);\r\n\r\n/**\r\n * Resolves the element id under the pointer that is eligible to receive\r\n * an append-after drop. Returns null when:\r\n * - nothing renderable is under the pointer,\r\n * - the hit is chrome (palette, ghost, overlays),\r\n * - the hit is an edge / pool / lane / data / annotation,\r\n * - the hit is an EXPANDED sub-process container (#8185): a drop\r\n * there belongs INSIDE it, which the place-at-cursor path handles\r\n * (`createShape` parents into the innermost container at the point),\r\n * - the hit is a placement surface (#8233): a drop there belongs AT THE\r\n * CURSOR, as a sibling, which the same place-at-cursor path handles.\r\n *\r\n * The pointer resolves to the INNERMOST rendered shape, so a drop onto\r\n * a task that lives inside an expanded sub-process still appends after\r\n * that task - only the container's own body falls through.\r\n */\r\nexport function findAppendTargetId(\r\n doc: Document,\r\n clientX: number,\r\n clientY: number,\r\n): string | null {\r\n // elementFromPoint sees the ghost preview at the top - temporarily\r\n // remove pointer events on it (we already set pointerEvents:none, so\r\n // this is for paranoia / browser quirks).\r\n const hit = doc.elementFromPoint(clientX, clientY);\r\n if (!hit) return null;\r\n if (hit.closest('[data-mz-chrome]')) return null;\r\n const shape = hit.closest('[data-element-id]');\r\n if (!shape) return null;\r\n if (isExpandedContainer(shape)) return null;\r\n const type = shape.getAttribute('data-type') ?? '';\r\n if (PLACEMENT_SURFACE_TYPES.has(type)) return null;\r\n if (!APPEND_ELIGIBLE_TYPES.has(type)) return null;\r\n return shape.getAttribute('data-element-id');\r\n}\r\n\r\n/**\r\n * Phase 11c: resolves the sequence-flow id under the pointer for the\r\n * split-and-insert path. Only sequence flows participate; message\r\n * flows and associations are skipped (splicing a new flow node into a\r\n * cross-pool message flow or an artifact association would change\r\n * semantic meaning).\r\n *\r\n * The hit relies on the wide invisible `.mz-flow-hit` path under each\r\n * flow (added in renderers/flows.ts) so users can drop anywhere within\r\n * +/- 9px of the visible line and still register an edge drop.\r\n */\r\nexport function findSplitEdgeId(\r\n doc: Document,\r\n clientX: number,\r\n clientY: number,\r\n): string | null {\r\n const hit = doc.elementFromPoint(clientX, clientY);\r\n if (!hit) return null;\r\n if (hit.closest('[data-mz-chrome]')) return null;\r\n const edge = hit.closest('[data-element-id]');\r\n if (!edge) return null;\r\n const type = edge.getAttribute('data-type') ?? '';\r\n if (type !== 'sequenceFlow') return null;\r\n return edge.getAttribute('data-element-id');\r\n}\r\n\r\nexport const HIGHLIGHT_CLASS = 'mz-palette-drop-target';\r\n\r\nexport function addHighlight(node: Element): void {\r\n node.classList.add(HIGHLIGHT_CLASS);\r\n}\r\n\r\nexport function clearHighlight(node: Element): void {\r\n node.classList.remove(HIGHLIGHT_CLASS);\r\n}\r\n\r\n/**\r\n * Shared with the arm-then-place gesture (`palette-arm.ts`): resolves\r\n * the highlightable drop-target node under the given client point,\r\n * preferring an append-eligible shape over a splittable edge. Returns\r\n * null when the point is over empty canvas or chrome.\r\n */\r\nexport function findDropTargetNode(\r\n doc: Document,\r\n clientX: number,\r\n clientY: number,\r\n): Element | null {\r\n const appendTargetId = findAppendTargetId(doc, clientX, clientY);\r\n if (appendTargetId) {\r\n return doc.querySelector(`[data-element-id=\"${cssEscape(appendTargetId)}\"]`);\r\n }\r\n const edgeId = findSplitEdgeId(doc, clientX, clientY);\r\n if (edgeId) {\r\n return doc.querySelector(`[data-element-id=\"${cssEscape(edgeId)}\"]`);\r\n }\r\n return null;\r\n}\r\n\r\nfunction cssEscape(id: string): string {\r\n if (typeof globalThis.CSS !== 'undefined' && typeof globalThis.CSS.escape === 'function') {\r\n return globalThis.CSS.escape(id);\r\n }\r\n return id.replace(/\"/g, '\\\\\"');\r\n}\r\n\r\nexport interface ClickSuppressorOptions {\r\n /**\r\n * CSS selector for click targets the suppressor must let THROUGH. A\r\n * matching click is not swallowed and dismisses the suppressor. The\r\n * arm-then-place gesture passes the palette-chrome selector so a stale\r\n * suppressor can never eat a palette click (#8202); the drag gesture\r\n * omits it because the click it must swallow IS a palette click.\r\n */\r\n readonly passThrough?: string;\r\n}\r\n\r\n/**\r\n * Installs a one-shot capture-phase click listener on `root` that calls\r\n * `stopImmediatePropagation` on the trailing click of the gesture that\r\n * installed it. The palette's own click handler runs in bubble phase so\r\n * it never sees the event - which is what we want, otherwise\r\n * pointerup-after-drag would also trigger the legacy auto-place path,\r\n * and the placement click would reach the pipeline's click-select\r\n * handler and clear the selection we just set.\r\n *\r\n * The suppressor is scoped to ITS OWN gesture and can never survive into\r\n * the next one (#8202). That trailing click is not guaranteed to be\r\n * generated at all - the arm controller cancels its own pointerdown and\r\n * the canvas re-renders underneath it, so the browser may never pair a\r\n * click - which used to leave the suppressor armed until it swallowed\r\n * the user's next palette click. Three exits guarantee it dies with its\r\n * gesture:\r\n *\r\n * 1. the trailing click arrives -> swallowed, suppressor removed\r\n * 2. a new pointer gesture starts -> removed on that pointerdown,\r\n * which always precedes that gesture's own click\r\n * 3. a `passThrough` click arrives -> let through, suppressor removed\r\n * (covers keyboard-activated clicks, which have no pointerdown)\r\n */\r\nexport function installClickSuppressor(\r\n root: HTMLElement,\r\n options?: ClickSuppressorOptions,\r\n): void {\r\n const passThrough = options?.passThrough;\r\n\r\n function dismiss(): void {\r\n root.removeEventListener('click', onClick, true);\r\n root.removeEventListener('pointerdown', onPointerDown, true);\r\n }\r\n\r\n function onClick(ev: Event): void {\r\n dismiss();\r\n if (passThrough !== undefined) {\r\n const target = ev.target as Element | null;\r\n if (target?.closest(passThrough)) return;\r\n }\r\n ev.stopImmediatePropagation();\r\n }\r\n\r\n function onPointerDown(): void {\r\n dismiss();\r\n }\r\n\r\n root.addEventListener('click', onClick, true);\r\n root.addEventListener('pointerdown', onPointerDown, true);\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Style injection */\r\n/* ------------------------------------------------------------------ */\r\n\r\nconst DRAG_STYLE_ID = 'mz-palette-drag-styles';\r\n\r\n/**\r\n * Drop-target highlight styles. Injected lazily on first attach so the\r\n * stylesheet does not depend on `attachPalette` calling it explicitly.\r\n */\r\nexport function injectPaletteDragStyles(doc: Document | null): void {\r\n if (!doc) return;\r\n if (doc.getElementById(DRAG_STYLE_ID)) return;\r\n const style = doc.createElement('style');\r\n style.id = DRAG_STYLE_ID;\r\n // Filter-based highlight: works regardless of the renderer's internal\r\n // shape class names. The drop shadow paints a soft glow around every\r\n // child of the target group, giving the user clear visual feedback\r\n // without us having to mirror every per-renderer class.\r\n style.textContent = `\r\n .${HIGHLIGHT_CLASS} {\r\n filter: drop-shadow(0 0 6px rgba(99, 102, 241, 0.95)) drop-shadow(0 0 10px rgba(99, 102, 241, 0.55));\r\n }\r\n .mz-palette-ghost {\r\n transition: transform 0.05s ease;\r\n }\r\n `;\r\n doc.head.appendChild(style);\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - palette arm-then-place gesture\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: click-to-arm placement from the palette (#8137). Clicking\r\n * a palette tool ARMS a cursor-following ghost of the chosen element;\r\n * the next click on the canvas places it exactly where the pointer is,\r\n * then the facade auto-selects it. This is the same \"pick the kind, then\r\n * place it\" model bpmn.io / Camunda / ARIS use, and it converges with the\r\n * drag gesture: both a drag drop and an armed place resolve through the\r\n * identical target logic (append-on-shape, split-on-edge, place-on-canvas)\r\n * and dispatch the SAME `PaletteDropPayload`. It replaces the old blind\r\n * auto-place on click, which cascaded the shape off-screen with no cursor\r\n * feedback and no scroll-into-view.\r\n *\r\n * The gesture is intentionally kept out of `palette.ts` so the two\r\n * controllers (drag = press-and-hold, arm = click-then-click) stay small\r\n * and share the placement primitives exported from `palette-drag.ts`.\r\n *\r\n * Resolution order on the placement click (most specific first):\r\n * 1. pointer over an append-eligible shape -> append-after\r\n * 2. pointer over a sequence flow -> split-and-insert\r\n * 3. pointer over empty canvas -> place at cursor scene xy\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { CreateShapeType } from '../api/shape-types.js';\r\nimport type { Viewport } from './viewport.js';\r\nimport { paletteIconFor } from './palette-icons.js';\r\nimport {\r\n buildGhost,\r\n positionGhost,\r\n clientToScene,\r\n findAppendTargetId,\r\n findSplitEdgeId,\r\n findDropTargetNode,\r\n addHighlight,\r\n clearHighlight,\r\n installClickSuppressor,\r\n type PaletteDropPayload,\r\n} from './palette-drag.js';\r\n\r\nexport interface PaletteArmOptions {\r\n /** The palette root element (returned by `attachPalette`). */\r\n paletteRoot: HTMLElement;\r\n /** The editor host container (parent of the canvas SVG + the palette). */\r\n container: HTMLElement;\r\n /**\r\n * The live viewport controller (or null while nothing is mounted).\r\n * Needed for the pointer -> scene transform on an empty-canvas place.\r\n * Mirrors the drag gesture's requirement (#7862): the transform is\r\n * anchored to the SVG canvas origin, which only the viewport knows.\r\n */\r\n getViewport: () => Viewport | null;\r\n /** Invoked on a successful place with the resolved drop payload. */\r\n onDrop: (payload: PaletteDropPayload) => void;\r\n /**\r\n * Notified whenever the armed state flips. The palette uses this to\r\n * paint / clear the armed highlight on the originating tool button.\r\n */\r\n onStateChange?: (armed: boolean) => void;\r\n}\r\n\r\nexport interface PaletteArmController {\r\n /** Arm placement for `type`. Re-arming swaps the ghost in place. */\r\n arm: (type: CreateShapeType) => void;\r\n /** Cancel any armed placement (Esc, re-selection, teardown). */\r\n cancel: () => void;\r\n /** True while a placement is armed. */\r\n isArmed: () => boolean;\r\n /** Remove every listener and clear any live ghost. */\r\n dispose: () => void;\r\n}\r\n\r\n/** Class toggled on the container while a placement is armed. */\r\nconst ARMING_CLASS = 'mz-palette-arming';\r\n\r\ninterface ArmState {\r\n type: CreateShapeType;\r\n ghost: HTMLElement;\r\n highlighted: Element | null;\r\n}\r\n\r\n/**\r\n * Installs the arm-then-place gesture. Returns a controller the palette\r\n * drives (`arm` on a tool click) plus a teardown.\r\n */\r\nexport function attachPaletteArm(options: PaletteArmOptions): PaletteArmController {\r\n const { paletteRoot, container, getViewport, onDrop, onStateChange } = options;\r\n const doc = paletteRoot.ownerDocument;\r\n let state: ArmState | null = null;\r\n\r\n injectArmStyles(doc);\r\n\r\n function setHighlight(node: Element | null): void {\r\n if (!state || state.highlighted === node) return;\r\n if (state.highlighted) {\r\n clearHighlight(state.highlighted);\r\n }\r\n state.highlighted = node;\r\n if (node) {\r\n addHighlight(node);\r\n }\r\n }\r\n\r\n function arm(type: CreateShapeType): void {\r\n if (state) {\r\n // Re-arm: keep the same ghost element, swap its glyph + type.\r\n state.type = type;\r\n setHighlight(null);\r\n state.ghost.innerHTML = paletteIconFor(type);\r\n const swappedSvg = state.ghost.querySelector('svg');\r\n if (swappedSvg) {\r\n swappedSvg.setAttribute('width', '32');\r\n swappedSvg.setAttribute('height', '32');\r\n (swappedSvg as SVGSVGElement).style.pointerEvents = 'none';\r\n }\r\n return;\r\n }\r\n const ghost = buildGhost(doc, type);\r\n container.appendChild(ghost);\r\n container.classList.add(ARMING_CLASS);\r\n state = { type, ghost, highlighted: null };\r\n onStateChange?.(true);\r\n }\r\n\r\n function cancel(): void {\r\n if (!state) return;\r\n if (state.highlighted) {\r\n clearHighlight(state.highlighted);\r\n }\r\n if (state.ghost.parentNode === container) {\r\n container.removeChild(state.ghost);\r\n }\r\n container.classList.remove(ARMING_CLASS);\r\n state = null;\r\n onStateChange?.(false);\r\n }\r\n\r\n function resolvePayload(clientX: number, clientY: number): PaletteDropPayload | null {\r\n if (!state) return null;\r\n const type = state.type;\r\n const appendTargetId = findAppendTargetId(doc, clientX, clientY);\r\n if (appendTargetId) {\r\n return { type, appendSourceId: appendTargetId };\r\n }\r\n const splitEdgeId = findSplitEdgeId(doc, clientX, clientY);\r\n if (splitEdgeId) {\r\n return { type, splitEdgeId };\r\n }\r\n const scene = clientToScene(clientX, clientY, getViewport());\r\n if (!scene) return null;\r\n return { type, sceneX: scene.x, sceneY: scene.y };\r\n }\r\n\r\n // Placement is resolved on POINTERDOWN in the capture phase on the\r\n // container. Capturing on the container (an ancestor of the SVG) lets\r\n // us stop the event before the canvas subsystems act on it: shape-drag\r\n // (pointerdown on the SVG), marquee, pan, and the pipeline click\r\n // selection all run later and would otherwise start a drag or clear the\r\n // selection we are about to set. We then swallow the trailing click so\r\n // the pipeline's click-select handler cannot deselect the placed shape.\r\n const onPointerDown = (ev: PointerEvent): void => {\r\n if (!state) return;\r\n if (ev.button !== undefined && ev.button !== 0) return;\r\n const target = ev.target as Element | null;\r\n // A pointerdown on palette chrome (another tool, the caret, the\r\n // flyout) is not a placement - let the palette handle it. The ghost\r\n // stays armed; a body/variant click will re-arm it in place.\r\n if (target && target.closest('[data-mz-chrome]')) return;\r\n\r\n ev.preventDefault();\r\n ev.stopImmediatePropagation();\r\n const payload = resolvePayload(ev.clientX, ev.clientY);\r\n if (!payload) {\r\n // No viewport / unresolvable point - abort cleanly rather than\r\n // leaving the user stuck armed with no way to place.\r\n cancel();\r\n return;\r\n }\r\n // Swallow the click that this pointerdown will spawn so the pipeline\r\n // selection handler does not run and clear the fresh selection. The\r\n // click is NOT guaranteed to be spawned (we just cancelled the\r\n // pointerdown and the canvas re-renders underneath it), so the\r\n // suppressor is scoped to this gesture: it dies on the next\r\n // pointerdown, and it never swallows a palette click. Without that\r\n // scoping it outlived the placement and ate the user's next palette\r\n // click, forcing every tool to be clicked twice (#8202).\r\n installClickSuppressor(container, { passThrough: '[data-mz-chrome]' });\r\n cancel();\r\n onDrop(payload);\r\n };\r\n\r\n const onPointerMove = (ev: PointerEvent): void => {\r\n if (!state) return;\r\n positionGhost(state.ghost, container, ev.clientX, ev.clientY);\r\n const target = ev.target as Element | null;\r\n // Do not highlight when hovering the palette itself.\r\n if (target && target.closest('[data-mz-chrome]')) {\r\n setHighlight(null);\r\n return;\r\n }\r\n setHighlight(findDropTargetNode(doc, ev.clientX, ev.clientY));\r\n };\r\n\r\n const onKeyDown = (ev: KeyboardEvent): void => {\r\n if (!state || ev.key !== 'Escape') return;\r\n ev.preventDefault();\r\n ev.stopImmediatePropagation();\r\n cancel();\r\n };\r\n\r\n container.addEventListener('pointerdown', onPointerDown, { capture: true });\r\n container.addEventListener('pointermove', onPointerMove);\r\n container.addEventListener('keydown', onKeyDown, { capture: true });\r\n\r\n return {\r\n arm,\r\n cancel,\r\n isArmed: (): boolean => state !== null,\r\n dispose: (): void => {\r\n cancel();\r\n container.removeEventListener('pointerdown', onPointerDown, { capture: true });\r\n container.removeEventListener('pointermove', onPointerMove);\r\n container.removeEventListener('keydown', onKeyDown, { capture: true });\r\n },\r\n };\r\n}\r\n\r\nconst ARM_STYLE_ID = 'mz-palette-arm-styles';\r\n\r\n/**\r\n * Cursor styling for the arming state. A crosshair (rather than a hidden\r\n * cursor) keeps a visible pointer alongside the ghost so the user always\r\n * has an anchor for where the next click will land.\r\n */\r\nfunction injectArmStyles(doc: Document | null): void {\r\n if (!doc) return;\r\n if (doc.getElementById(ARM_STYLE_ID)) return;\r\n const style = doc.createElement('style');\r\n style.id = ARM_STYLE_ID;\r\n style.textContent = `\r\n .${ARMING_CLASS},\r\n .${ARMING_CLASS} svg { cursor: crosshair; }\r\n `;\r\n doc.head.appendChild(style);\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - palette / toolbox sidebar\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: an opt-in vertical (or horizontal) toolbox of clickable\r\n * shape icons mounted as a sibling DOM element inside the editor's\r\n * container. Two rendering modes are supported: a tiered \"grouped\" UI\r\n * (default) with ~10 primary buttons and variant flyouts, and a legacy\r\n * \"flat\" list (`palette: { grouped: false }`) showing every shape in\r\n * `shapes` as a top-level button. The palette never calls createShape\r\n * directly — the facade routes the chosen type so the command stack\r\n * semantics live in one place.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n *\r\n * Grouped UX detail:\r\n * - Each primary represents a family (Task, Gateway, Intermediate,\r\n * Data, …). Clicking the primary creates the family's DEFAULT\r\n * shape. When variants exist a small chevron is shown bottom-right\r\n * — hover 350ms or click it to open a flyout listing every variant.\r\n * Selecting a variant creates that subtype and closes the flyout.\r\n * - Only one flyout is open at a time. Outside-click and Esc close.\r\n *\r\n * Backward-compat:\r\n * - Variant flyout panels are present in the DOM (hidden until open),\r\n * so selectors like `[data-testid=\"palette-item-userTask\"]` still\r\n * resolve and clicks still dispatch even when closed.\r\n * - Pass `palette: { grouped: false }` for the original flat list.\r\n */\r\n\r\nimport type { CreateShapeType } from '../api/shape-types.js';\r\nimport type { Viewport } from './viewport.js';\r\nimport { englishTranslator } from '../i18n/translator.js';\r\nimport type { Translator } from '../i18n/types.js';\r\nimport {\r\n paletteCategoryFor,\r\n paletteIconFor,\r\n type PaletteCategory,\r\n} from './palette-icons.js';\r\nimport {\r\n attachPaletteDrag,\r\n injectPaletteDragStyles,\r\n type PaletteDropPayload,\r\n} from './palette-drag.js';\r\nimport {\r\n attachPaletteArm,\r\n type PaletteArmController,\r\n} from './palette-arm.js';\r\n\r\n/** Position of the palette inside the host container. */\r\nexport type PalettePosition = 'left' | 'right' | 'top';\r\n\r\n/**\r\n * Palette display mode. `narrow` is icon-only and 48px wide — minimal\r\n * canvas intrusion, expert use. `wide` shows icon + text labels +\r\n * section headers and is ~200px wide — discoverable for new users.\r\n * The user can flip between them via the in-palette chevron, and the\r\n * choice is persisted in localStorage under `mz-palette-mode`.\r\n */\r\nexport type PaletteMode = 'narrow' | 'wide';\r\n\r\n/** Resolved palette configuration the renderer accepts. */\r\nexport interface ResolvedPaletteConfig {\r\n /** Where to mount the palette inside the host container. Default 'left'. */\r\n position: PalettePosition;\r\n /**\r\n * Shape roster shown as buttons (only honored when `grouped` is false).\r\n * In grouped mode this is ignored — the canonical PALETTE_GROUPS\r\n * structure drives the layout.\r\n */\r\n shapes: readonly CreateShapeType[];\r\n /** Track width (vertical) or height (horizontal) in CSS pixels. Default 48. */\r\n thickness: number;\r\n /** Wide-mode track width (or height when position='top'). Default 200. */\r\n wideThickness: number;\r\n /** Initial mode at mount time. Overridden by localStorage if present. */\r\n mode: PaletteMode;\r\n /**\r\n * When true (default), render the new tiered UI: ~10 primary buttons\r\n * with variant flyouts. When false, render the legacy flat list of\r\n * every shape in `shapes`.\r\n */\r\n grouped: boolean;\r\n}\r\n\r\n/** Public configuration the constructor accepts (all fields optional). */\r\nexport interface PaletteConfig {\r\n /** Where to mount the palette. Default `'left'`. */\r\n position?: PalettePosition;\r\n /**\r\n * Shape roster shown as buttons, in display order. Honored only when\r\n * `grouped: false`. Default = the full CreateShapeType roster.\r\n *\r\n * In grouped mode this option is ignored: the canonical group/variant\r\n * structure (PALETTE_GROUPS) is the single source of truth so the\r\n * compact UX is consistent across all consumers.\r\n */\r\n shapes?: readonly CreateShapeType[];\r\n /**\r\n * Track width (vertical) / height (horizontal) in CSS pixels.\r\n * Default 48 — fits 24x24 icons with 12px padding on each side.\r\n */\r\n thickness?: number;\r\n /** Wide-mode track size in CSS pixels. Default 200. */\r\n wideThickness?: number;\r\n /** Initial mode (narrow / wide). Default 'narrow'. */\r\n mode?: PaletteMode;\r\n /**\r\n * Tiered (grouped) UI vs the legacy flat list. Default `true` —\r\n * the new UX wraps task / gateway / intermediate-event / data\r\n * variants behind primary buttons with hover flyouts so the toolbox\r\n * shows ~10 buttons instead of 24+. Pass `false` to opt back into\r\n * the legacy flat layout (e.g. for niche embedders that want every\r\n * shape one click away).\r\n */\r\n grouped?: boolean;\r\n}\r\n\r\n/**\r\n * The full default roster, in the canonical authoring order:\r\n * events first (chronologically: start, intermediates, end), then\r\n * tasks (generic to specialised), then gateways (most-common first),\r\n * then containers / data / artifacts. Consumers can pass a subset\r\n * via `PaletteConfig.shapes` if they want a smaller toolbox (only\r\n * honored in `grouped: false` mode).\r\n */\r\nexport const DEFAULT_PALETTE_ROSTER: readonly CreateShapeType[] = [\r\n // Events\r\n 'startEvent',\r\n 'intermediateThrowEvent',\r\n 'intermediateCatchEvent',\r\n 'endEvent',\r\n // Tasks\r\n 'task',\r\n 'userTask',\r\n 'serviceTask',\r\n 'sendTask',\r\n 'receiveTask',\r\n 'manualTask',\r\n 'businessRuleTask',\r\n 'scriptTask',\r\n 'callActivity',\r\n // Gateways\r\n 'exclusiveGateway',\r\n 'parallelGateway',\r\n 'inclusiveGateway',\r\n 'eventBasedGateway',\r\n 'complexGateway',\r\n // Containers / data / artifacts\r\n 'subProcess',\r\n 'adHocSubProcess',\r\n 'transaction',\r\n 'dataObject',\r\n 'dataObjectReference',\r\n 'dataStoreReference',\r\n 'textAnnotation',\r\n 'group',\r\n];\r\n\r\n/**\r\n * A primary entry in the tiered palette. `defaultType` is the shape\r\n * created when the user clicks the primary itself; `variants` lists\r\n * additional subtypes shown in the flyout (excluding the default).\r\n *\r\n * `kind: 'shape'` primaries route through `onCreate(CreateShapeType)`.\r\n * `kind: 'structural'` primaries route through `onCreateParticipant()`\r\n * or `onCreateLane()` and have no variants.\r\n */\r\nexport interface PaletteGroup {\r\n /** Stable identifier — used in CSS classes and data-testid. */\r\n key: string;\r\n /** Display name (tooltip + aria-label on the primary button). */\r\n label: string;\r\n /** Whether this primary creates a CreateShapeType or a structural element. */\r\n kind: 'shape' | 'structural';\r\n /**\r\n * For shape primaries: the CreateShapeType created when the primary\r\n * is clicked. For structural primaries this is the structural key\r\n * ('participant' | 'lane') used by the dispatch handler.\r\n */\r\n defaultType: CreateShapeType | 'participant' | 'lane';\r\n /**\r\n * Additional CreateShapeType variants shown in the flyout. Empty\r\n * when the primary has no alternatives. Structural primaries always\r\n * have an empty variants list.\r\n */\r\n variants: readonly CreateShapeType[];\r\n}\r\n\r\n/**\r\n * The canonical group/variant structure for the tiered palette. ~10\r\n * primaries across events, tasks, gateways, sub-processes, data, and\r\n * structural containers. Order is left-to-right (or top-to-bottom)\r\n * authoring order — the same flow as DEFAULT_PALETTE_ROSTER, just\r\n * collapsed.\r\n *\r\n * The Pool / Lane structural entries are appended only when the host\r\n * facade supplies the matching `onCreateParticipant` / `onCreateLane`\r\n * callbacks (see `attachPalette`). They live in this same list so the\r\n * grouped renderer can treat them uniformly.\r\n */\r\nexport const PALETTE_GROUPS: readonly PaletteGroup[] = [\r\n {\r\n key: 'start',\r\n label: 'Start event',\r\n kind: 'shape',\r\n defaultType: 'startEvent',\r\n variants: [],\r\n },\r\n {\r\n key: 'intermediate',\r\n label: 'Intermediate event',\r\n kind: 'shape',\r\n defaultType: 'intermediateThrowEvent',\r\n variants: ['intermediateCatchEvent'],\r\n },\r\n {\r\n key: 'end',\r\n label: 'End event',\r\n kind: 'shape',\r\n defaultType: 'endEvent',\r\n variants: [],\r\n },\r\n {\r\n key: 'task',\r\n label: 'Task',\r\n kind: 'shape',\r\n defaultType: 'task',\r\n variants: [\r\n 'userTask',\r\n 'serviceTask',\r\n 'sendTask',\r\n 'receiveTask',\r\n 'manualTask',\r\n 'businessRuleTask',\r\n 'scriptTask',\r\n 'callActivity',\r\n ],\r\n },\r\n {\r\n key: 'gateway',\r\n label: 'Gateway',\r\n kind: 'shape',\r\n defaultType: 'exclusiveGateway',\r\n variants: [\r\n 'parallelGateway',\r\n 'inclusiveGateway',\r\n 'eventBasedGateway',\r\n 'complexGateway',\r\n ],\r\n },\r\n {\r\n key: 'subprocess',\r\n label: 'Sub-process',\r\n kind: 'shape',\r\n defaultType: 'subProcess',\r\n variants: ['adHocSubProcess', 'transaction'],\r\n },\r\n {\r\n key: 'data',\r\n label: 'Data object',\r\n kind: 'shape',\r\n defaultType: 'dataObject',\r\n variants: ['dataObjectReference', 'dataStoreReference'],\r\n },\r\n {\r\n key: 'annotation',\r\n label: 'Text annotation',\r\n kind: 'shape',\r\n defaultType: 'textAnnotation',\r\n variants: ['group'],\r\n },\r\n];\r\n\r\n/**\r\n * Flyout entries that need a second, descriptive line under their name\r\n * (#8345). Two shape types in the same family can be legitimately drawn\r\n * with the SAME symbol — BPMN 2.0 gives `dataObject` and\r\n * `dataObjectReference` the identical page-with-folded-corner glyph —\r\n * so no icon can tell the user which one to pick. The difference is\r\n * conceptual, so it is stated in words instead of invented in pixels.\r\n *\r\n * Types absent from this map render exactly as before: icon + name, one\r\n * line, no layout change.\r\n */\r\nconst VARIANT_DESCRIPTION_KEYS: Readonly>> = {\r\n dataObject: 'type.dataObject.description',\r\n dataObjectReference: 'type.dataObjectReference.description',\r\n};\r\n\r\n/**\r\n * Structural primaries (Pool, Lane) appended after the shape groups\r\n * when the matching facade callback is supplied. Kept separate so the\r\n * caller can include or omit them per host without rewriting the\r\n * canonical `PALETTE_GROUPS` list.\r\n */\r\nconst STRUCTURAL_GROUPS: Readonly> = {\r\n participant: {\r\n key: 'pool',\r\n label: 'Pool',\r\n kind: 'structural',\r\n defaultType: 'participant',\r\n variants: [],\r\n },\r\n lane: {\r\n key: 'lane',\r\n label: 'Lane',\r\n kind: 'structural',\r\n defaultType: 'lane',\r\n variants: [],\r\n },\r\n};\r\n\r\n/**\r\n * Hover-to-open delay for primary-with-variants flyouts. 350ms is the\r\n * informal industry sweet spot — long enough to ignore accidental\r\n * hovers when the user is moving the cursor across the toolbox, short\r\n * enough that an intentional hover-to-explore feels responsive.\r\n */\r\nconst HOVER_OPEN_DELAY_MS = 350;\r\n\r\n/**\r\n * Normalise the constructor-supplied option (boolean | object | undefined)\r\n * into a fully-resolved config that the renderer can consume directly.\r\n * Returns null when the palette is disabled.\r\n */\r\nexport function resolvePaletteConfig(\r\n option: boolean | PaletteConfig | undefined,\r\n): ResolvedPaletteConfig | null {\r\n if (option === undefined || option === false) return null;\r\n const cfg = option === true ? {} : option;\r\n return {\r\n position: cfg.position ?? 'left',\r\n shapes: cfg.shapes ?? DEFAULT_PALETTE_ROSTER,\r\n thickness: cfg.thickness ?? 48,\r\n // Wide mode needs to fit the longest label end-to-end. The\r\n // longest in DEFAULT_PALETTE_ROSTER is \"Intermediate throw event\"\r\n // (24 chars). At 13px font the text alone is ~165px; add the\r\n // 24px icon, 10+10px padding, 10px gap, and ~6px for the\r\n // scrollbar = ~225px. 260px gives ~30px of breathing room so\r\n // browser/font hinting variations can never clip a label.\r\n wideThickness: cfg.wideThickness ?? 260,\r\n mode: cfg.mode ?? 'narrow',\r\n // Default to the new tiered UI. Hosts that explicitly want the\r\n // old long flat list pass `grouped: false`.\r\n grouped: cfg.grouped ?? true,\r\n };\r\n}\r\n\r\ninterface AttachPaletteOptions {\r\n /** Editor host container; the palette is appended as a sibling element. */\r\n container: HTMLElement;\r\n /** Resolved palette config. */\r\n config: ResolvedPaletteConfig;\r\n /**\r\n * Invoked with the chosen shape type when an icon is clicked. The\r\n * palette does not call createShape directly — the facade owns that\r\n * so the command stack semantics live in one place.\r\n *\r\n * Phase 11a/b widening: callers may pass an optional `opts` payload.\r\n * No opts = legacy click path (facade auto-places). With\r\n * `appendSourceId` = drop-on-shape (facade appends + connects).\r\n * With `sceneX`/`sceneY` = drop-on-canvas (facade places at cursor).\r\n */\r\n onCreate: (type: CreateShapeType, opts?: PaletteDropPayload) => void;\r\n /**\r\n * Optional viewport-controller getter. When supplied, the palette\r\n * mounts the drag-to-create gesture (Phase 11a). Without it the\r\n * palette stays click-only and the existing auto-place behaviour\r\n * runs unchanged. The gesture needs the live controller (not a state\r\n * snapshot) for the SVG-anchored pointer-to-scene transform (#7862).\r\n */\r\n getViewport?: () => Viewport | null;\r\n /**\r\n * Invoked when the user clicks the Pool button. Pools are NOT in\r\n * `CreateShapeType` because they need extra plumbing (collaboration,\r\n * processRef) — the facade owns the dedicated `createParticipant`\r\n * method.\r\n */\r\n onCreateParticipant?: () => void;\r\n /**\r\n * Invoked when the user clicks the Lane button. Lanes are also\r\n * special: they need a target pool, so the facade resolves it from\r\n * the current selection or first-pool fallback.\r\n */\r\n onCreateLane?: () => void;\r\n /**\r\n * Optional override of the document host (defaults to the container's\r\n * owner document). Mirrors RenderOptions.document so unit tests can\r\n * inject jsdom.\r\n */\r\n document?: Document;\r\n /**\r\n * Translator for palette labels / tooltips / aria text. Defaults to\r\n * the built-in English translator when omitted (standalone tests,\r\n * embedders that never set a language). The facade always supplies\r\n * one built from `BpmnEditorOptions.language`.\r\n */\r\n translate?: Translator;\r\n}\r\n\r\ninterface AttachedPalette {\r\n /** The mounted root element. Exposed so consumers can inspect it. */\r\n element: HTMLElement;\r\n /** Tear-down: remove the element and detach listeners. */\r\n dispose: () => void;\r\n}\r\n\r\n/**\r\n * Mount the palette inside `container`. Idempotent at the call site —\r\n * caller is expected to dispose any previous instance before remount.\r\n */\r\nexport function attachPalette(\r\n options: AttachPaletteOptions,\r\n): AttachedPalette {\r\n const doc = options.document ?? options.container.ownerDocument;\r\n if (!doc) {\r\n throw new Error('attachPalette: container has no ownerDocument');\r\n }\r\n const t: Translator = options.translate ?? englishTranslator;\r\n // Initial mode: config default, overridden by localStorage when\r\n // present so the user's earlier choice survives reloads. Wrapped in\r\n // try/catch because localStorage may be unavailable (jsdom, privacy\r\n // mode, sandboxed iframes).\r\n let mode: PaletteMode = options.config.mode;\r\n try {\r\n const stored = doc.defaultView?.localStorage?.getItem('mz-palette-mode');\r\n if (stored === 'narrow' || stored === 'wide') {\r\n mode = stored;\r\n }\r\n } catch {\r\n // Ignore — fall back to the config default.\r\n }\r\n\r\n const root = doc.createElement('div');\r\n root.className =\r\n `mz-palette mz-palette--${options.config.position} mz-palette--${mode} ` +\r\n `mz-palette--${options.config.grouped ? 'grouped' : 'flat'}`;\r\n root.setAttribute('data-mz-chrome', 'true');\r\n root.setAttribute('data-testid', 'palette');\r\n root.setAttribute('role', 'toolbar');\r\n root.setAttribute('aria-label', t('palette.aria.toolbar'));\r\n root.setAttribute('aria-orientation',\r\n options.config.position === 'top' ? 'horizontal' : 'vertical');\r\n applyTrackSizing(root, options.config, mode);\r\n\r\n // Toggle chevron at the top — anchors the palette and survives\r\n // scroll. The actual mode flip happens in the click handler so the\r\n // single delegated handler dispatches every kind of click.\r\n const toggleBtn = buildToggleButton(doc, t);\r\n updateToggleGlyph(toggleBtn, mode);\r\n root.appendChild(toggleBtn);\r\n\r\n // Flyout state: at most one flyout is open at any time. The\r\n // `cleanup` function removes document-level listeners that close\r\n // the flyout on outside-click / Esc.\r\n const flyoutState: FlyoutState = {\r\n primaryButton: null,\r\n panel: null,\r\n docCleanup: null,\r\n hoverTimer: null,\r\n };\r\n\r\n // Arm-then-place controller (#8137). Mounted below only when the host\r\n // supplies a viewport getter (the facade always does); a click then\r\n // ARMS a cursor-following ghost instead of blind-dropping the shape.\r\n // Bare embedders without a viewport keep the legacy click-to-create.\r\n let armController: PaletteArmController | null = null;\r\n // The palette tool button currently painted as armed, if any.\r\n let armedButton: HTMLButtonElement | null = null;\r\n\r\n const setArmedButton = (button: HTMLButtonElement | null): void => {\r\n if (armedButton === button) return;\r\n if (armedButton) {\r\n armedButton.classList.remove('mz-palette-item--armed');\r\n }\r\n armedButton = button;\r\n if (button) {\r\n button.classList.add('mz-palette-item--armed');\r\n }\r\n };\r\n\r\n // Resolve the primary tool button that owns a variant flyout item, so\r\n // we can paint the armed highlight on the visible tool (the variant\r\n // itself lives inside a flyout that closes on selection).\r\n const ownerPrimaryFor = (variant: HTMLElement): HTMLButtonElement | null => {\r\n const panel = variant.closest('.mz-palette-flyout');\r\n const ownerKey = panel?.getAttribute('data-owner-key');\r\n if (!ownerKey) return null;\r\n return root.querySelector(\r\n `button[data-mz-primary=\"${cssEscape(ownerKey)}\"]`,\r\n );\r\n };\r\n\r\n // Route a chosen type to the arm controller when present, else fall\r\n // back to the legacy immediate create (no-viewport embedders).\r\n const dispatchChosen = (\r\n type: CreateShapeType,\r\n armTarget: HTMLButtonElement | null,\r\n ): void => {\r\n if (armController) {\r\n setArmedButton(armTarget);\r\n armController.arm(type);\r\n } else {\r\n options.onCreate(type);\r\n }\r\n };\r\n\r\n if (options.config.grouped) {\r\n renderGrouped(doc, root, options, t);\r\n } else {\r\n renderFlat(doc, root, options, t);\r\n }\r\n\r\n // Stop pointer events on the palette from bubbling to the editor's\r\n // viewport. The viewport listens for `pointerdown` on the host\r\n // container (palette is a child of that container) and calls\r\n // `setPointerCapture` on the container, which redirects every\r\n // subsequent pointer event for the gesture (including pointerup) to\r\n // the container — so the browser fires `click` on the common ancestor\r\n // (the host), not the button. Without this, clicks on palette\r\n // buttons are silently swallowed. We stop pointerdown only — leaving\r\n // `click` to bubble normally so the delegated handler below picks it\r\n // up — and we stop `mousedown` for parity with non-PointerEvent paths.\r\n const onPointerDown = (ev: Event): void => {\r\n ev.stopPropagation();\r\n };\r\n root.addEventListener('pointerdown', onPointerDown);\r\n root.addEventListener('mousedown', onPointerDown);\r\n\r\n const onClick = (ev: MouseEvent): void => {\r\n const target = ev.target as Element | null;\r\n if (!target) return;\r\n\r\n // Toggle button takes precedence — it lives in the palette but\r\n // does not create a shape.\r\n const toggle = target.closest('button.mz-palette-toggle');\r\n if (toggle) {\r\n ev.preventDefault();\r\n ev.stopPropagation();\r\n closeFlyout(flyoutState);\r\n const next: PaletteMode = mode === 'narrow' ? 'wide' : 'narrow';\r\n mode = next;\r\n root.classList.toggle('mz-palette--narrow', next === 'narrow');\r\n root.classList.toggle('mz-palette--wide', next === 'wide');\r\n applyTrackSizing(root, options.config, next);\r\n updateToggleGlyph(toggleBtn, next);\r\n try {\r\n doc.defaultView?.localStorage?.setItem('mz-palette-mode', next);\r\n } catch {\r\n // Persistence failed — runtime state is still correct.\r\n }\r\n return;\r\n }\r\n\r\n // Chevron click on a primary-with-variants opens the flyout\r\n // immediately (no hover delay). The chevron is a span inside the\r\n // primary button so we test for it before the generic primary path.\r\n const chevron = target.closest('.mz-palette-chevron');\r\n if (chevron) {\r\n const primary = chevron.closest('button.mz-palette-item');\r\n if (primary && primary.dataset['hasVariants'] === 'true') {\r\n ev.preventDefault();\r\n ev.stopPropagation();\r\n toggleFlyoutFor(primary, root, flyoutState, doc);\r\n return;\r\n }\r\n }\r\n\r\n // Variant click (inside an open flyout) — arm the variant shape for\r\n // placement and close the flyout. The armed highlight is painted on\r\n // the owning primary tool since the flyout is about to close.\r\n const variant = target.closest('button.mz-palette-variant');\r\n if (variant) {\r\n ev.preventDefault();\r\n ev.stopPropagation();\r\n const type = variant.getAttribute('data-shape-type') as CreateShapeType | null;\r\n const owner = ownerPrimaryFor(variant);\r\n closeFlyout(flyoutState);\r\n if (type) {\r\n dispatchChosen(type, owner);\r\n }\r\n return;\r\n }\r\n\r\n // Primary button (split-button body) click — ARMS the family's\r\n // default shape for placement and closes any open flyout. The wide\r\n // caret zone (handled above) is the affordance for the variant\r\n // menu; the body always arms the default so the common case stays\r\n // one gesture. Structural primaries (Pool / Lane) keep their\r\n // immediate create — they have bespoke placement of their own.\r\n const button = target.closest('button.mz-palette-item');\r\n if (!button) return;\r\n ev.preventDefault();\r\n ev.stopPropagation();\r\n closeFlyout(flyoutState);\r\n const structural = button.getAttribute('data-structural');\r\n if (structural === 'participant') {\r\n options.onCreateParticipant?.();\r\n return;\r\n }\r\n if (structural === 'lane') {\r\n options.onCreateLane?.();\r\n return;\r\n }\r\n const type = button.getAttribute('data-shape-type') as CreateShapeType | null;\r\n if (!type) return;\r\n dispatchChosen(type, button);\r\n };\r\n\r\n // Hover-to-open: only relevant for primaries-with-variants in\r\n // grouped mode. We scope the listener to the root and dispatch by\r\n // closest() so the handler set is small and stable across re-renders.\r\n const onPointerEnter = (ev: PointerEvent): void => {\r\n if (!options.config.grouped) return;\r\n const target = ev.target as Element | null;\r\n if (!target) return;\r\n const primary = target.closest('button.mz-palette-item');\r\n if (!primary || primary.dataset['hasVariants'] !== 'true') return;\r\n // Cancel any prior pending open — the user moved to a different\r\n // primary before the timer fired.\r\n if (flyoutState.hoverTimer !== null) {\r\n doc.defaultView?.clearTimeout(flyoutState.hoverTimer);\r\n flyoutState.hoverTimer = null;\r\n }\r\n flyoutState.hoverTimer = doc.defaultView?.setTimeout(() => {\r\n flyoutState.hoverTimer = null;\r\n // Re-check that the primary is still in the DOM before opening\r\n // — the editor may have disposed in the interim.\r\n if (primary.isConnected) {\r\n openFlyoutFor(primary, root, flyoutState, doc);\r\n }\r\n }, HOVER_OPEN_DELAY_MS) ?? null;\r\n };\r\n\r\n const onPointerLeave = (ev: PointerEvent): void => {\r\n // Cancel any pending open if the user leaves the palette before\r\n // the timer fires.\r\n const related = ev.relatedTarget as Element | null;\r\n if (related && root.contains(related)) return;\r\n if (flyoutState.hoverTimer !== null) {\r\n doc.defaultView?.clearTimeout(flyoutState.hoverTimer);\r\n flyoutState.hoverTimer = null;\r\n }\r\n };\r\n\r\n // Keyboard navigation. Right-arrow on a primary-with-variants opens\r\n // its flyout; Up/Down inside a flyout cycles the menuitems; Enter\r\n // activates; Esc closes.\r\n const onKeyDown = (ev: KeyboardEvent): void => {\r\n const target = ev.target as Element | null;\r\n if (!target) return;\r\n\r\n if (ev.key === 'Escape' && flyoutState.panel) {\r\n ev.preventDefault();\r\n ev.stopPropagation();\r\n // Move focus back to the primary that owns this flyout so the\r\n // user can keep tabbing.\r\n const primary = flyoutState.primaryButton;\r\n closeFlyout(flyoutState);\r\n primary?.focus();\r\n return;\r\n }\r\n\r\n if (ev.key === 'ArrowRight' && options.config.grouped) {\r\n const primary = target.closest('button.mz-palette-item');\r\n if (primary && primary.dataset['hasVariants'] === 'true') {\r\n ev.preventDefault();\r\n openFlyoutFor(primary, root, flyoutState, doc);\r\n const firstItem = flyoutState.panel?.querySelector(\r\n 'button.mz-palette-variant',\r\n );\r\n firstItem?.focus();\r\n return;\r\n }\r\n }\r\n\r\n if ((ev.key === 'ArrowDown' || ev.key === 'ArrowUp') && flyoutState.panel) {\r\n const items = Array.from(\r\n flyoutState.panel.querySelectorAll(\r\n 'button.mz-palette-variant',\r\n ),\r\n );\r\n if (items.length === 0) return;\r\n const idx = items.findIndex((item) => item === target);\r\n if (idx === -1) return;\r\n ev.preventDefault();\r\n const delta = ev.key === 'ArrowDown' ? 1 : -1;\r\n const next = items[(idx + delta + items.length) % items.length];\r\n next?.focus();\r\n }\r\n };\r\n\r\n root.addEventListener('click', onClick);\r\n root.addEventListener('pointerenter', onPointerEnter, true);\r\n root.addEventListener('pointerleave', onPointerLeave);\r\n root.addEventListener('keydown', onKeyDown);\r\n\r\n // Mount as the first child of the container so it visually precedes\r\n // the SVG canvas in flex / block layouts. The container's CSS\r\n // (display: flex etc.) is the consumer's responsibility, but our\r\n // default styling sets up a flex row when position is left/right.\r\n ensureContainerLayout(options.container, options.config.position);\r\n options.container.insertBefore(root, options.container.firstChild);\r\n\r\n // Phase 11a/b: drag-from-palette gesture. Mounted only when the host\r\n // supplies a viewport getter (the facade does; jsdom unit tests that\r\n // don't need the gesture just omit it). The gesture cooperates with\r\n // the click handler above: a true click (pointer barely moved) falls\r\n // through to onClick = legacy auto-place; a drag (pointer moved past\r\n // threshold) calls options.onCreate(type, opts) with a drop payload.\r\n let detachDrag: (() => void) | null = null;\r\n if (options.getViewport) {\r\n const getViewport = options.getViewport;\r\n injectPaletteDragStyles(doc);\r\n // Arm-then-place (#8137): a plain palette click arms a cursor-\r\n // following ghost and the next canvas click places the shape.\r\n // Shares the placement primitives with the drag gesture so click\r\n // and drag converge on one create path.\r\n armController = attachPaletteArm({\r\n paletteRoot: root,\r\n container: options.container,\r\n getViewport,\r\n onDrop: (payload) => {\r\n options.onCreate(payload.type, payload);\r\n },\r\n onStateChange: (armed) => {\r\n if (!armed) setArmedButton(null);\r\n },\r\n });\r\n detachDrag = attachPaletteDrag({\r\n paletteRoot: root,\r\n container: options.container,\r\n getViewport,\r\n onDrop: (payload) => {\r\n // A drag supersedes any armed click state.\r\n armController?.cancel();\r\n options.onCreate(payload.type, payload);\r\n },\r\n });\r\n }\r\n\r\n return {\r\n element: root,\r\n dispose: (): void => {\r\n closeFlyout(flyoutState);\r\n if (flyoutState.hoverTimer !== null) {\r\n doc.defaultView?.clearTimeout(flyoutState.hoverTimer);\r\n flyoutState.hoverTimer = null;\r\n }\r\n detachDrag?.();\r\n armController?.dispose();\r\n armController = null;\r\n root.removeEventListener('click', onClick);\r\n root.removeEventListener('pointerdown', onPointerDown);\r\n root.removeEventListener('mousedown', onPointerDown);\r\n root.removeEventListener('pointerenter', onPointerEnter, true);\r\n root.removeEventListener('pointerleave', onPointerLeave);\r\n root.removeEventListener('keydown', onKeyDown);\r\n if (root.parentNode === options.container) {\r\n options.container.removeChild(root);\r\n }\r\n // Restore container layout flag we may have set so a remount\r\n // does not stack `data-mz-palette-host` indefinitely.\r\n restoreContainerLayout(options.container);\r\n },\r\n };\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Renderers — grouped (default) and flat (legacy opt-out) */\r\n/* ------------------------------------------------------------------ */\r\n\r\nfunction renderGrouped(\r\n doc: Document,\r\n root: HTMLElement,\r\n options: AttachPaletteOptions,\r\n t: Translator,\r\n): void {\r\n // Build the effective primary list: the canonical PALETTE_GROUPS\r\n // plus structural Pool / Lane primaries when the host supplies the\r\n // matching callbacks.\r\n const groups: PaletteGroup[] = [...PALETTE_GROUPS];\r\n if (options.onCreateParticipant) {\r\n groups.push(STRUCTURAL_GROUPS.participant);\r\n }\r\n if (options.onCreateLane) {\r\n groups.push(STRUCTURAL_GROUPS.lane);\r\n }\r\n\r\n for (const group of groups) {\r\n const primary = buildPrimaryButton(doc, group, t);\r\n root.appendChild(primary);\r\n if (group.variants.length > 0) {\r\n const panel = buildFlyoutPanel(doc, group, t);\r\n // Flyouts live inside the palette root so click delegation and\r\n // disposal stay simple. They are positioned absolutely relative\r\n // to the root so the browser does not need a re-flow when they\r\n // open. CSS hides them until `--open` is added.\r\n root.appendChild(panel);\r\n // Cross-link primary <-> panel for the hover/click open paths.\r\n primary.dataset['flyoutId'] = panel.id;\r\n panel.dataset['ownerKey'] = group.key;\r\n }\r\n }\r\n}\r\n\r\nfunction renderFlat(\r\n doc: Document,\r\n root: HTMLElement,\r\n options: AttachPaletteOptions,\r\n t: Translator,\r\n): void {\r\n // Section headers above each new category, plus the buttons. In\r\n // narrow mode the headers are display:none; the order is the same\r\n // either way so toggling is visually stable.\r\n let lastCategory: PaletteCategory | null = null;\r\n for (const type of options.config.shapes) {\r\n const cat = paletteCategoryFor(type);\r\n if (cat !== lastCategory) {\r\n root.appendChild(buildSectionHeader(doc, t(`palette.category.${cat.toLowerCase()}`)));\r\n lastCategory = cat;\r\n }\r\n root.appendChild(buildShapeItem(doc, type, t));\r\n }\r\n // Structural buttons (Pool, Lane) live AFTER the per-shape buttons\r\n // under their own \"Structure\" header. They aren't in `CreateShapeType`\r\n // because they take a different facade entry point — Pools need to\r\n // synthesise a collaboration, Lanes need a target pool. The facade\r\n // passes these callbacks in.\r\n if (options.onCreateParticipant || options.onCreateLane) {\r\n root.appendChild(buildSectionHeader(doc, t('palette.section.structure')));\r\n }\r\n if (options.onCreateParticipant) {\r\n root.appendChild(buildStructuralItem(doc, 'participant', t));\r\n }\r\n if (options.onCreateLane) {\r\n root.appendChild(buildStructuralItem(doc, 'lane', t));\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Button factories */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Resolves the translation key for a group's primary label. Shape\r\n * groups use the generic family name (`palette.group.`, e.g.\r\n * \"Gateway\"); structural groups (Pool / Lane) use\r\n * `palette.structural.`.\r\n */\r\nfunction paletteGroupLabelKey(group: PaletteGroup): string {\r\n return group.kind === 'structural'\r\n ? `palette.structural.${group.key}`\r\n : `palette.group.${group.key}`;\r\n}\r\n\r\n/**\r\n * Builds the primary button for a tiered-palette group. For shape\r\n * groups this carries `data-shape-type` (the default variant) and\r\n * `data-testid=\"palette-primary-\"`; for primaries that have\r\n * variants it also carries `data-has-variants=\"true\"`, a chevron\r\n * affordance in the bottom-right corner, and the ARIA menu attributes.\r\n */\r\nfunction buildPrimaryButton(\r\n doc: Document,\r\n group: PaletteGroup,\r\n t: Translator,\r\n): HTMLButtonElement {\r\n const label = t(paletteGroupLabelKey(group));\r\n const button = doc.createElement('button');\r\n button.type = 'button';\r\n button.className = 'mz-palette-item mz-palette-primary';\r\n button.setAttribute('data-mz-chrome', 'true');\r\n button.setAttribute('data-testid', `palette-primary-${group.key}`);\r\n button.title = label;\r\n button.setAttribute('aria-label', label);\r\n\r\n const hasVariants = group.variants.length > 0;\r\n if (hasVariants) {\r\n button.dataset['hasVariants'] = 'true';\r\n button.setAttribute('aria-haspopup', 'menu');\r\n button.setAttribute('aria-expanded', 'false');\r\n }\r\n\r\n // Canonical testid scheme:\r\n // - Primaries WITH variants carry the new `palette-primary-`\r\n // testid since the variant flyout is the user-facing affordance\r\n // for picking subtypes. The default-shape variant for that\r\n // family (e.g. `task`) lives in the flyout under\r\n // `palette-item-task` so the legacy selector keeps resolving.\r\n // - Primaries WITHOUT variants (Start, End, Sub-process,\r\n // Annotation, Pool, Lane) are functionally identical to the\r\n // legacy flat items, so they keep the `palette-item-` /\r\n // `palette-item-` testid. Existing tests that\r\n // `getByTestId('palette-item-startEvent')` resolve to these\r\n // primaries unchanged.\r\n //\r\n // `data-mz-primary=\"\"` is exposed on every primary as a\r\n // structural hook for new tests that want to enumerate the tier-1\r\n // toolbox without caring about the testid scheme.\r\n button.setAttribute('data-mz-primary', group.key);\r\n button.setAttribute(\r\n 'data-testid',\r\n hasVariants\r\n ? `palette-primary-${group.key}`\r\n : group.kind === 'shape'\r\n ? `palette-item-${String(group.defaultType)}`\r\n : `palette-item-${String(group.defaultType)}`,\r\n );\r\n\r\n if (group.kind === 'shape') {\r\n const type = group.defaultType as CreateShapeType;\r\n button.setAttribute('data-shape-type', type);\r\n button.innerHTML =\r\n `${paletteIconFor(type)}` +\r\n `${escapeHtml(label)}` +\r\n (hasVariants ? chevronAffordance(group.variants.length + 1) : '');\r\n } else {\r\n const structural = group.defaultType as 'participant' | 'lane';\r\n button.setAttribute('data-structural', structural);\r\n const iconSvg = structural === 'participant' ? POOL_ICON : LANE_ICON;\r\n button.innerHTML =\r\n `${iconSvg}` +\r\n `${escapeHtml(label)}`;\r\n }\r\n return button;\r\n}\r\n\r\nlet flyoutIdSeq = 0;\r\n\r\n/**\r\n * Builds the flyout panel that lists a primary's variant subtypes.\r\n * The panel is hidden by default (CSS) and revealed by adding the\r\n * `mz-palette-flyout--open` class. Items carry both the legacy\r\n * `palette-item-` testid and the new `palette-variant-`\r\n * testid so existing tests keep resolving.\r\n */\r\nfunction buildFlyoutPanel(\r\n doc: Document,\r\n group: PaletteGroup,\r\n t: Translator,\r\n): HTMLDivElement {\r\n const panel = doc.createElement('div');\r\n flyoutIdSeq += 1;\r\n panel.id = `mz-palette-flyout-${String(flyoutIdSeq)}`;\r\n panel.className = 'mz-palette-flyout';\r\n panel.setAttribute('data-mz-chrome', 'true');\r\n panel.setAttribute('data-testid', `palette-flyout-${group.key}`);\r\n panel.setAttribute('role', 'menu');\r\n panel.setAttribute(\r\n 'aria-label',\r\n t('palette.flyout.variantsSuffix', { label: t(paletteGroupLabelKey(group)) }),\r\n );\r\n panel.setAttribute('aria-hidden', 'true');\r\n\r\n // The default variant is included in the flyout too — opening the\r\n // flyout should expose every option in the family, not just the\r\n // alternatives. This matches Figma / Adobe / VS Code conventions\r\n // where the default appears at the top of its own flyout.\r\n const allVariants: CreateShapeType[] = [\r\n group.defaultType as CreateShapeType,\r\n ...group.variants,\r\n ];\r\n for (const type of allVariants) {\r\n panel.appendChild(buildVariantButton(doc, type, t));\r\n }\r\n return panel;\r\n}\r\n\r\nfunction buildVariantButton(\r\n doc: Document,\r\n type: CreateShapeType,\r\n t: Translator,\r\n): HTMLButtonElement {\r\n const button = doc.createElement('button');\r\n button.type = 'button';\r\n button.className = 'mz-palette-variant';\r\n button.setAttribute('data-mz-chrome', 'true');\r\n button.setAttribute('data-shape-type', type);\r\n button.setAttribute('role', 'menuitem');\r\n button.setAttribute('tabindex', '-1');\r\n // The variant carries the LEGACY `palette-item-` testid as\r\n // its canonical `data-testid` so existing tests and embedders that\r\n // target `[data-testid=\"palette-item-userTask\"]` (or use\r\n // `getByTestId('palette-item-userTask')`) keep working without\r\n // change. The new `palette-variant-` selector is exposed via\r\n // a sibling `data-mz-variant=\"\"` attribute — new tests can\r\n // query `[data-mz-variant=\"userTask\"]` to disambiguate from a flat-\r\n // mode primary.\r\n button.setAttribute('data-testid', `palette-item-${type}`);\r\n button.setAttribute('data-mz-variant', type);\r\n const label = t(`type.${type}`);\r\n // A description is only present for families whose members share a\r\n // glyph (see VARIANT_DESCRIPTION_KEYS). When present it becomes a\r\n // second visible line AND is appended to the tooltip / accessible\r\n // name, so the distinction reaches sighted and screen-reader users\r\n // alike rather than living in a hover-only affordance.\r\n const descriptionKey = VARIANT_DESCRIPTION_KEYS[type];\r\n const description = descriptionKey === undefined ? '' : t(descriptionKey);\r\n button.title = description.length > 0 ? `${label} - ${description}` : label;\r\n button.setAttribute(\r\n 'aria-label',\r\n description.length > 0 ? `${label} - ${description}` : label,\r\n );\r\n button.innerHTML =\r\n `${paletteIconFor(type)}` +\r\n `` +\r\n `${escapeHtml(label)}` +\r\n (description.length > 0\r\n ? `${escapeHtml(description)}`\r\n : '') +\r\n ``;\r\n return button;\r\n}\r\n\r\n/**\r\n * Legacy flat-mode shape button (one per CreateShapeType in the\r\n * `shapes` roster). Same shape as the original implementation.\r\n */\r\nfunction buildShapeItem(\r\n doc: Document,\r\n type: CreateShapeType,\r\n t: Translator,\r\n): HTMLButtonElement {\r\n const button = doc.createElement('button');\r\n button.type = 'button';\r\n button.className = 'mz-palette-item';\r\n button.setAttribute('data-mz-chrome', 'true');\r\n button.setAttribute('data-shape-type', type);\r\n button.setAttribute('data-testid', `palette-item-${type}`);\r\n const label = t(`type.${type}`);\r\n button.title = label;\r\n button.setAttribute('aria-label', label);\r\n button.innerHTML =\r\n `${paletteIconFor(type)}` +\r\n `${escapeHtml(label)}`;\r\n return button;\r\n}\r\n\r\n/**\r\n * Builds a palette button for a \"structural\" element (Pool or Lane)\r\n * that needs special facade plumbing — they aren't `CreateShapeType`s.\r\n * The click handler dispatches via `data-structural` rather than\r\n * `data-shape-type`. Used by the legacy flat renderer; the grouped\r\n * renderer uses `buildPrimaryButton` for these too.\r\n */\r\nfunction buildStructuralItem(\r\n doc: Document,\r\n kind: 'participant' | 'lane',\r\n t: Translator,\r\n): HTMLButtonElement {\r\n const label = t(kind === 'participant' ? 'palette.structural.pool' : 'palette.structural.lane');\r\n const button = doc.createElement('button');\r\n button.type = 'button';\r\n button.className = 'mz-palette-item';\r\n button.setAttribute('data-mz-chrome', 'true');\r\n button.setAttribute('data-structural', kind);\r\n button.setAttribute('data-testid', `palette-item-${kind}`);\r\n button.title = label;\r\n button.setAttribute('aria-label', label);\r\n const iconSvg = kind === 'participant' ? POOL_ICON : LANE_ICON;\r\n button.innerHTML =\r\n `${iconSvg}` +\r\n `${escapeHtml(label)}`;\r\n return button;\r\n}\r\n\r\n/** Minimal HTML escape for the few characters that could appear in\r\n * display names. Display names come from SHAPE_DISPLAY_NAMES (a\r\n * controlled, English-only literal) so the risk surface is tiny, but\r\n * we escape anyway for defence-in-depth. */\r\nfunction escapeHtml(s: string): string {\r\n return s\r\n .replace(/&/g, '&')\r\n .replace(//g, '>')\r\n .replace(/\"/g, '"');\r\n}\r\n\r\n/** Section divider rendered above the first button of each category in\r\n * wide mode. Hidden by CSS in narrow mode. Used by the flat renderer. */\r\nfunction buildSectionHeader(doc: Document, label: string): HTMLDivElement {\r\n const div = doc.createElement('div');\r\n div.className = 'mz-palette-section';\r\n div.setAttribute('data-mz-chrome', 'true');\r\n div.textContent = label;\r\n return div;\r\n}\r\n\r\n/** The chevron toggle button mounted at the top of the palette. */\r\nfunction buildToggleButton(doc: Document, t: Translator): HTMLButtonElement {\r\n const btn = doc.createElement('button');\r\n btn.type = 'button';\r\n btn.className = 'mz-palette-toggle';\r\n btn.setAttribute('data-mz-chrome', 'true');\r\n btn.setAttribute('data-testid', 'palette-toggle');\r\n const toggleLabel = t('palette.toggle.title');\r\n btn.title = toggleLabel;\r\n btn.setAttribute('aria-label', toggleLabel);\r\n // Glyph is updated by the toggle handler. Use » in narrow (expand)\r\n // and « in wide (collapse).\r\n btn.innerHTML = '»';\r\n return btn;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Flyout open / close */\r\n/* ------------------------------------------------------------------ */\r\n\r\ninterface FlyoutState {\r\n primaryButton: HTMLButtonElement | null;\r\n panel: HTMLDivElement | null;\r\n /** Document-level click + keydown removers, installed while a flyout is open. */\r\n docCleanup: (() => void) | null;\r\n /** Pending hover-to-open setTimeout id, or null. */\r\n hoverTimer: number | null;\r\n}\r\n\r\nfunction openFlyoutFor(\r\n primary: HTMLButtonElement,\r\n root: HTMLElement,\r\n state: FlyoutState,\r\n doc: Document,\r\n): void {\r\n const flyoutId = primary.dataset['flyoutId'];\r\n if (!flyoutId) return;\r\n const panel = root.querySelector(`#${cssEscape(flyoutId)}`);\r\n if (!panel) return;\r\n\r\n // Already open for this primary — no-op.\r\n if (state.panel === panel) return;\r\n // A different flyout was open — close it first.\r\n if (state.panel) closeFlyout(state);\r\n\r\n state.primaryButton = primary;\r\n state.panel = panel;\r\n primary.classList.add('mz-palette-primary--open');\r\n primary.setAttribute('aria-expanded', 'true');\r\n panel.classList.add('mz-palette-flyout--open');\r\n panel.setAttribute('aria-hidden', 'false');\r\n positionFlyout(primary, panel, root);\r\n\r\n // Document-level listeners for outside-click and Esc-while-focus-not-in-palette.\r\n // We use capture on click so we close BEFORE any other handler that\r\n // might re-open us.\r\n const onDocClick = (ev: Event): void => {\r\n const target = ev.target as Node | null;\r\n if (!target) return;\r\n if (panel.contains(target)) return;\r\n if (primary.contains(target)) return;\r\n closeFlyout(state);\r\n };\r\n const onDocKey = (ev: KeyboardEvent): void => {\r\n if (ev.key === 'Escape') {\r\n closeFlyout(state);\r\n primary.focus();\r\n }\r\n };\r\n doc.addEventListener('click', onDocClick, true);\r\n doc.addEventListener('keydown', onDocKey);\r\n state.docCleanup = (): void => {\r\n doc.removeEventListener('click', onDocClick, true);\r\n doc.removeEventListener('keydown', onDocKey);\r\n };\r\n}\r\n\r\nfunction toggleFlyoutFor(\r\n primary: HTMLButtonElement,\r\n root: HTMLElement,\r\n state: FlyoutState,\r\n doc: Document,\r\n): void {\r\n if (state.primaryButton === primary) {\r\n closeFlyout(state);\r\n return;\r\n }\r\n openFlyoutFor(primary, root, state, doc);\r\n}\r\n\r\nfunction closeFlyout(state: FlyoutState): void {\r\n if (state.panel) {\r\n state.panel.classList.remove('mz-palette-flyout--open');\r\n state.panel.setAttribute('aria-hidden', 'true');\r\n }\r\n if (state.primaryButton) {\r\n state.primaryButton.classList.remove('mz-palette-primary--open');\r\n state.primaryButton.setAttribute('aria-expanded', 'false');\r\n }\r\n state.docCleanup?.();\r\n state.panel = null;\r\n state.primaryButton = null;\r\n state.docCleanup = null;\r\n}\r\n\r\n/** Gap (px) between the primary button edge and the flyout panel. */\r\nconst FLYOUT_GAP_PX = 8;\r\n\r\n/**\r\n * Minimum gap (px) the clamp keeps between the flyout and the viewport\r\n * edges so a menu is never flush against — or bleeding past — the edge.\r\n */\r\nconst FLYOUT_VIEWPORT_MARGIN_PX = 8;\r\n\r\n/**\r\n * Stacking level for an OPEN flyout. Because the panel is positioned\r\n * `fixed` (see positionFlyout) it leaves the palette's own stacking\r\n * context, so it needs a value high enough to sit above the canvas /\r\n * editor chrome, while staying below app-level modals.\r\n */\r\nconst FLYOUT_Z_INDEX = '1000';\r\n\r\n/**\r\n * Positions the flyout next to its primary button and clamps it into the\r\n * viewport (mirroring bpmn.io's `_ensureVisible`).\r\n *\r\n * The panel is a DOM child of the palette root, and the root sets\r\n * `overflow-y:auto; overflow-x:hidden` (see applyTrackSizing) so it can\r\n * scroll its shape roster. A flyout opening to the SIDE of the palette is\r\n * therefore clipped away by that overflow box — the historic bug (#8131):\r\n * the chevron opened the menu but it painted off-clip / invisible.\r\n *\r\n * Fix: position the panel `fixed` so it is laid out against the viewport\r\n * (the initial containing block) rather than inside the palette's\r\n * overflow box. No ancestor of the palette establishes a containing block\r\n * for fixed elements (none set transform/filter/perspective/contain — the\r\n * chain is `.bpmn-host` -> `.bpm-editor-canvas` -> `.bpm-editor-page`), so\r\n * `fixed` reliably escapes every ancestor `overflow` clip. The panel stays\r\n * a child of the root in the DOM, keeping click delegation, disposal, and\r\n * the backward-compat `data-testid` selectors intact.\r\n *\r\n * After the preferred placement is chosen (to the right of a left palette,\r\n * to the left of a right palette, below a top palette), the panel is\r\n * measured and clamped so it never overflows a viewport edge: it flips to\r\n * the opposite side when the preferred side would overflow horizontally,\r\n * and its top is shifted up so the whole menu stays on screen.\r\n */\r\nfunction positionFlyout(\r\n primary: HTMLButtonElement,\r\n panel: HTMLDivElement,\r\n root: HTMLElement,\r\n): void {\r\n const primRect = primary.getBoundingClientRect();\r\n const isTopPalette = root.classList.contains('mz-palette--top');\r\n const isRightPalette = root.classList.contains('mz-palette--right');\r\n\r\n // Lift the panel out of the palette's overflow box. Measuring while\r\n // pinned to (0,0) yields the panel's intrinsic size independent of the\r\n // final placement (fixed size does not depend on left/top).\r\n panel.style.position = 'fixed';\r\n panel.style.zIndex = FLYOUT_Z_INDEX;\r\n panel.style.right = '';\r\n panel.style.left = '0px';\r\n panel.style.top = '0px';\r\n\r\n const panelRect = panel.getBoundingClientRect();\r\n const panelWidth = panelRect.width;\r\n const panelHeight = panelRect.height;\r\n\r\n const view = root.ownerDocument.defaultView;\r\n const viewportWidth = view?.innerWidth ?? root.ownerDocument.documentElement.clientWidth;\r\n const viewportHeight = view?.innerHeight ?? root.ownerDocument.documentElement.clientHeight;\r\n const margin = FLYOUT_VIEWPORT_MARGIN_PX;\r\n\r\n let left: number;\r\n let top: number;\r\n if (isTopPalette) {\r\n // Drop down below the primary button.\r\n left = primRect.left;\r\n top = primRect.bottom + FLYOUT_GAP_PX;\r\n } else if (isRightPalette) {\r\n // Right palette — open to the LEFT of the button.\r\n left = primRect.left - FLYOUT_GAP_PX - panelWidth;\r\n top = primRect.top;\r\n } else {\r\n // Default left palette — open to the RIGHT of the button.\r\n left = primRect.right + FLYOUT_GAP_PX;\r\n top = primRect.top;\r\n }\r\n\r\n // Horizontal clamp: if the preferred side overflows, flip to the other\r\n // side of the primary; if that still overflows, pin inside the margin.\r\n if (!isTopPalette) {\r\n if (left + panelWidth > viewportWidth - margin) {\r\n const flippedLeft = primRect.left - FLYOUT_GAP_PX - panelWidth;\r\n left = flippedLeft >= margin ? flippedLeft : viewportWidth - margin - panelWidth;\r\n } else if (left < margin) {\r\n const flippedRight = primRect.right + FLYOUT_GAP_PX;\r\n left = flippedRight + panelWidth <= viewportWidth - margin ? flippedRight : margin;\r\n }\r\n }\r\n left = Math.min(Math.max(left, margin), Math.max(margin, viewportWidth - margin - panelWidth));\r\n\r\n // Vertical clamp: shift the menu up so its bottom stays on screen, then\r\n // keep its top below the top margin.\r\n if (top + panelHeight > viewportHeight - margin) {\r\n top = viewportHeight - margin - panelHeight;\r\n }\r\n top = Math.max(top, margin);\r\n\r\n panel.style.left = `${String(Math.round(left))}px`;\r\n panel.style.top = `${String(Math.round(top))}px`;\r\n}\r\n\r\n/**\r\n * CSS.escape polyfill for ID selectors. Called only with our own\r\n * generated IDs (`mz-palette-flyout-N`) so a minimal whitelist is\r\n * sufficient — we don't need to handle arbitrary user input.\r\n */\r\nfunction cssEscape(id: string): string {\r\n return id.replace(/[^a-zA-Z0-9_-]/g, '\\\\$&');\r\n}\r\n\r\n/**\r\n * The split-button caret zone shown on primaries that have variants.\r\n * The palette tool is a split button (#8137 visual spec): the button\r\n * body arms the family's default kind; this wide, divided caret zone on\r\n * the trailing edge opens the variant flyout. It carries a small count\r\n * badge (total kinds in the family) — surfaced in wide mode, hidden in\r\n * the icon-only narrow mode where the zone collapses to a corner glyph.\r\n */\r\nfunction chevronAffordance(kindCount: number): string {\r\n return (\r\n '' +\r\n `${String(kindCount)}` +\r\n '' +\r\n '' +\r\n '' +\r\n ''\r\n );\r\n}\r\n\r\nconst POOL_ICON =\r\n `` +\r\n // Outer rect = pool, with header strip on the left.\r\n `` +\r\n `` +\r\n ``;\r\n\r\nconst LANE_ICON =\r\n `` +\r\n // Pool with TWO horizontal lanes (the second one highlighted by a fill).\r\n `` +\r\n `` +\r\n `` +\r\n `` +\r\n ``;\r\n\r\nfunction applyTrackSizing(\r\n root: HTMLElement,\r\n cfg: ResolvedPaletteConfig,\r\n mode: PaletteMode,\r\n): void {\r\n const thickness = mode === 'wide' ? cfg.wideThickness : cfg.thickness;\r\n const px = `${String(thickness)}px`;\r\n if (cfg.position === 'top') {\r\n root.style.height = px;\r\n root.style.width = '100%';\r\n root.style.flexDirection = 'row';\r\n } else {\r\n root.style.width = px;\r\n root.style.height = '100%';\r\n root.style.flexDirection = 'column';\r\n }\r\n root.style.display = 'flex';\r\n // Wide mode aligns content to the start (icon-then-label rows fill\r\n // the whole row). Narrow mode keeps the existing centred layout.\r\n root.style.alignItems = mode === 'wide' ? 'stretch' : 'center';\r\n root.style.gap = mode === 'wide' ? '2px' : '4px';\r\n root.style.padding = mode === 'wide' ? '4px' : '6px';\r\n root.style.boxSizing = 'border-box';\r\n // The expanded roster (22+ shapes) easily exceeds typical canvas\r\n // height, so the palette must scroll when its content overflows.\r\n // We hide the scrollbar via CSS in injectPaletteItemStyles to keep\r\n // the toolbox visually clean.\r\n if (cfg.position !== 'top') {\r\n root.style.overflowY = 'auto';\r\n root.style.overflowX = 'hidden';\r\n } else {\r\n root.style.overflowX = 'auto';\r\n root.style.overflowY = 'hidden';\r\n }\r\n // Sit above the canvas chrome but allow shape clicks elsewhere.\r\n // `position: relative` is required so flyouts can position\r\n // themselves with `position: absolute` against this root.\r\n root.style.position = 'relative';\r\n root.style.zIndex = '5';\r\n // Default chrome look — host can override via CSS.\r\n root.style.background = 'rgba(248, 250, 252, 0.9)';\r\n root.style.borderRight = cfg.position === 'left' ? '1px solid rgba(0,0,0,0.08)' : '';\r\n root.style.borderLeft = cfg.position === 'right' ? '1px solid rgba(0,0,0,0.08)' : '';\r\n root.style.borderBottom = cfg.position === 'top' ? '1px solid rgba(0,0,0,0.08)' : '';\r\n injectPaletteItemStyles(root.ownerDocument);\r\n}\r\n\r\nconst PALETTE_STYLE_ID = 'mz-palette-styles';\r\n\r\nfunction injectPaletteItemStyles(doc: Document | null): void {\r\n if (!doc) return;\r\n if (doc.getElementById(PALETTE_STYLE_ID)) return;\r\n const style = doc.createElement('style');\r\n style.id = PALETTE_STYLE_ID;\r\n style.textContent = `\r\n .mz-palette-item {\r\n width: 36px;\r\n height: 36px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 0;\r\n border: 1px solid transparent;\r\n border-radius: 6px;\r\n background: transparent;\r\n color: var(--mz-color-text-default, #1f2937);\r\n cursor: pointer;\r\n flex: 0 0 auto;\r\n position: relative;\r\n transition: background-color 0.12s ease, border-color 0.12s ease;\r\n }\r\n .mz-palette-item:hover {\r\n background: rgba(99, 102, 241, 0.1);\r\n border-color: rgba(99, 102, 241, 0.3);\r\n }\r\n .mz-palette-item:focus {\r\n outline: none;\r\n border-color: rgba(99, 102, 241, 0.6);\r\n box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);\r\n }\r\n .mz-palette-item:active,\r\n .mz-palette-primary--open {\r\n background: rgba(99, 102, 241, 0.18);\r\n }\r\n /* Armed state (#8137): the tool whose ghost is following the cursor,\r\n waiting for a placement click. */\r\n .mz-palette-item--armed,\r\n .mz-palette-item--armed:hover {\r\n background: rgba(99, 102, 241, 0.16);\r\n border-color: rgba(99, 102, 241, 0.55);\r\n }\r\n .mz-palette-item svg {\r\n pointer-events: none;\r\n }\r\n /* Split-button caret zone on primaries with variants. In icon-only\r\n narrow mode it collapses to a corner glyph (the count is hidden);\r\n in wide mode it becomes a wide, divided trailing zone that reads\r\n as a separate control (see the wide-mode overrides below). Uses\r\n currentColor so it inherits the icon theming. */\r\n .mz-palette-chevron {\r\n position: absolute;\r\n right: 3px;\r\n bottom: 3px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: var(--mz-color-text-muted, rgba(0,0,0,0.55));\r\n pointer-events: auto;\r\n line-height: 0;\r\n }\r\n .mz-palette-chevron-glyph {\r\n width: 8px;\r\n height: 8px;\r\n }\r\n .mz-palette-chevron-count {\r\n display: none;\r\n }\r\n .mz-palette-item:hover .mz-palette-chevron,\r\n .mz-palette-primary--open .mz-palette-chevron {\r\n color: var(--mz-color-text-default, #1f2937);\r\n }\r\n\r\n /* Keep the scrollbar functional but visually subtle so the\r\n palette stays clean when the roster overflows. */\r\n .mz-palette {\r\n scrollbar-width: thin;\r\n scrollbar-color: rgba(0,0,0,0.18) transparent;\r\n }\r\n .mz-palette::-webkit-scrollbar { width: 6px; height: 6px; }\r\n .mz-palette::-webkit-scrollbar-thumb {\r\n background: rgba(0,0,0,0.18);\r\n border-radius: 3px;\r\n }\r\n .mz-palette::-webkit-scrollbar-track { background: transparent; }\r\n\r\n /* ---------- Narrow / Wide mode ---------- */\r\n\r\n /* The icon span wraps the per-shape SVG. In narrow mode it is the\r\n only visible element; in wide mode it sits on the left of the\r\n button next to the label. */\r\n .mz-palette-icon {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n flex: 0 0 auto;\r\n }\r\n\r\n .mz-palette-label {\r\n white-space: nowrap;\r\n font-size: 13px;\r\n line-height: 1;\r\n color: inherit;\r\n }\r\n\r\n /* Narrow mode hides text + section headers (the DOM is still\r\n there — flipping back to wide is just a CSS class change). */\r\n .mz-palette--narrow .mz-palette-label,\r\n .mz-palette--narrow .mz-palette-section { display: none; }\r\n\r\n /* Wide mode: full-width buttons with icon + text */\r\n .mz-palette--wide .mz-palette-item {\r\n width: 100%;\r\n height: 32px;\r\n justify-content: flex-start;\r\n gap: 10px;\r\n padding: 4px 10px;\r\n flex: 0 0 auto;\r\n }\r\n\r\n /* Wide-mode split button (#8137): the label fills the middle, and the\r\n caret becomes a wide divided trailing zone that opens the variant\r\n menu. The button padding-right is dropped so the divider reaches\r\n the button edge. */\r\n .mz-palette--wide .mz-palette-primary[data-has-variants='true'] {\r\n padding-right: 0;\r\n }\r\n .mz-palette--wide .mz-palette-primary[data-has-variants='true'] .mz-palette-label {\r\n flex: 1 1 auto;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n }\r\n .mz-palette--wide .mz-palette-primary[data-has-variants='true'] .mz-palette-chevron {\r\n position: static;\r\n align-self: stretch;\r\n flex: 0 0 36px;\r\n width: 36px;\r\n gap: 3px;\r\n border-left: 1px solid rgba(0, 0, 0, 0.10);\r\n border-top-right-radius: 6px;\r\n border-bottom-right-radius: 6px;\r\n }\r\n .mz-palette--wide .mz-palette-chevron-glyph {\r\n width: 12px;\r\n height: 12px;\r\n }\r\n .mz-palette--wide .mz-palette-chevron-count {\r\n display: inline-block;\r\n font-size: 9px;\r\n line-height: 1;\r\n font-variant-numeric: tabular-nums;\r\n opacity: 0.7;\r\n }\r\n /* Hover on the caret zone alone gives the split-button its two-target\r\n feel — the body arms, the caret opens the menu. */\r\n .mz-palette--wide .mz-palette-primary[data-has-variants='true'] .mz-palette-chevron:hover,\r\n .mz-palette--wide .mz-palette-primary--open .mz-palette-chevron {\r\n background: rgba(99, 102, 241, 0.14);\r\n color: var(--mz-color-text-default, #1f2937);\r\n }\r\n\r\n .mz-palette--wide .mz-palette-section {\r\n font-size: 11px;\r\n font-weight: 600;\r\n text-transform: uppercase;\r\n color: rgba(0,0,0,0.45);\r\n padding: 8px 10px 4px;\r\n letter-spacing: 0.04em;\r\n width: 100%;\r\n box-sizing: border-box;\r\n }\r\n\r\n /* Toggle chevron at the top of the palette */\r\n .mz-palette-toggle {\r\n width: 100%;\r\n height: 22px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: flex-end;\r\n padding: 0 6px;\r\n background: transparent;\r\n border: none;\r\n cursor: pointer;\r\n color: rgba(0,0,0,0.45);\r\n flex: 0 0 auto;\r\n }\r\n .mz-palette-toggle:hover { color: var(--mz-color-text-default, #1f2937); }\r\n .mz-palette-toggle-icon {\r\n font-size: 14px;\r\n line-height: 1;\r\n font-weight: 600;\r\n }\r\n\r\n /* ---------- Flyout panel ---------- */\r\n\r\n /* Flyouts live inside the palette root and float over neighboring\r\n canvas chrome. Hidden by default — CSS class flip toggles\r\n visibility so click delegation can dispatch even when closed\r\n (which keeps backward-compat selectors like\r\n data-testid=palette-item-userTask resolving). */\r\n .mz-palette-flyout {\r\n display: none;\r\n position: absolute;\r\n min-width: 180px;\r\n max-width: 280px;\r\n max-height: 360px;\r\n overflow-y: auto;\r\n padding: 4px;\r\n gap: 1px;\r\n background: var(--mz-color-surface-default, #ffffff);\r\n border: 1px solid rgba(0,0,0,0.12);\r\n border-radius: 6px;\r\n box-shadow: 0 6px 18px rgba(0,0,0,0.12), 0 1px 3px rgba(0,0,0,0.08);\r\n z-index: 10;\r\n }\r\n .mz-palette-flyout--open {\r\n display: flex;\r\n flex-direction: column;\r\n }\r\n\r\n /* Variant rows mirror the wide-mode primary rows: icon on the\r\n left, label to the right. They are NOT mz-palette-item, so they\r\n do not inflate the primary count in tests. */\r\n .mz-palette-variant {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 10px;\r\n width: 100%;\r\n min-height: 32px;\r\n padding: 4px 10px;\r\n border: 1px solid transparent;\r\n border-radius: 4px;\r\n background: transparent;\r\n color: var(--mz-color-text-default, #1f2937);\r\n cursor: pointer;\r\n text-align: left;\r\n flex: 0 0 auto;\r\n }\r\n .mz-palette-variant:hover {\r\n background: rgba(99, 102, 241, 0.1);\r\n border-color: rgba(99, 102, 241, 0.3);\r\n }\r\n .mz-palette-variant:focus {\r\n outline: none;\r\n border-color: rgba(99, 102, 241, 0.6);\r\n box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);\r\n }\r\n .mz-palette-variant svg {\r\n pointer-events: none;\r\n }\r\n .mz-palette-variant .mz-palette-label {\r\n display: inline-block;\r\n }\r\n\r\n /* Text column of a variant row. Single-line entries keep the exact\r\n former geometry (24px icon + 4px padding = the 32px row); entries\r\n that carry a description (#8345) stack name over description and\r\n let the row grow. */\r\n .mz-palette-variant-text {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: flex-start;\r\n justify-content: center;\r\n gap: 2px;\r\n min-width: 0;\r\n flex: 1 1 auto;\r\n }\r\n\r\n /* The disambiguating second line. Muted and smaller so the name\r\n stays the primary read. Wraps rather than truncating — a\r\n half-shown explanation explains nothing. */\r\n .mz-palette-variant-desc {\r\n font-size: 11px;\r\n line-height: 1.3;\r\n white-space: normal;\r\n color: var(--mz-color-text-muted, rgba(0,0,0,0.55));\r\n }\r\n `;\r\n doc.head.appendChild(style);\r\n}\r\n\r\n/** Updates the chevron glyph based on the current mode: » means\r\n * \"expand to wide\" (shown in narrow), « means \"collapse to narrow\"\r\n * (shown in wide). */\r\nfunction updateToggleGlyph(btn: HTMLButtonElement, mode: PaletteMode): void {\r\n const icon = btn.querySelector('.mz-palette-toggle-icon');\r\n if (icon) {\r\n icon.textContent = mode === 'wide' ? '«' : '»';\r\n }\r\n}\r\n\r\nfunction ensureContainerLayout(\r\n container: HTMLElement,\r\n position: PalettePosition,\r\n): void {\r\n // Mark so we know to clean up on dispose. If the container already\r\n // had a different display value we leave it alone.\r\n if (container.dataset['mzPaletteHost'] === 'true') return;\r\n container.dataset['mzPaletteHost'] = 'true';\r\n container.dataset['mzPalettePrevDisplay'] = container.style.display ?? '';\r\n container.dataset['mzPalettePrevFlexDirection'] = container.style.flexDirection ?? '';\r\n container.style.display = 'flex';\r\n container.style.flexDirection = position === 'top' ? 'column' : 'row';\r\n}\r\n\r\nfunction restoreContainerLayout(container: HTMLElement): void {\r\n if (container.dataset['mzPaletteHost'] !== 'true') return;\r\n const prevDisplay = container.dataset['mzPalettePrevDisplay'] ?? '';\r\n const prevFlexDir = container.dataset['mzPalettePrevFlexDirection'] ?? '';\r\n container.style.display = prevDisplay;\r\n container.style.flexDirection = prevFlexDir;\r\n delete container.dataset['mzPaletteHost'];\r\n delete container.dataset['mzPalettePrevDisplay'];\r\n delete container.dataset['mzPalettePrevFlexDirection'];\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - controls (toolbar) widget\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: an opt-in five-button toolbar (zoom in / zoom out /\r\n * fit / undo / redo) anchored to a corner of the editor container.\r\n * Mirrors the palette's mount-as-sibling-DOM-element pattern so the\r\n * library never has to know about the host's chrome stack.\r\n *\r\n * Buttons call back into the BpmnEditor facade - the toolbar itself\r\n * is pure DOM/SVG with no scene/model dependencies. A\r\n * `subscribeUndoRedoState` callback wires the facade's\r\n * `command-executed` event into the disabled state of undo / redo\r\n * buttons; the toolbar exposes the unsubscribe in its own dispose.\r\n *\r\n * Readonly: `undo` and `redo` are not rendered when `readonly` is\r\n * true, regardless of the per-button flags. Zoom / fit stay because\r\n * they're useful for view-only consumers.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type {\r\n ControlsConfig,\r\n ControlsOffset,\r\n ControlsPosition,\r\n} from '../api/controls-types.js';\r\nimport { englishTranslator } from '../i18n/translator.js';\r\nimport type { Translator } from '../i18n/types.js';\r\n\r\n/** Resolved toolbar configuration the renderer accepts. */\r\nexport interface ResolvedControlsConfig {\r\n position: ControlsPosition;\r\n offset: ControlsOffset;\r\n zoomIn: boolean;\r\n zoomOut: boolean;\r\n fit: boolean;\r\n undo: boolean;\r\n redo: boolean;\r\n routeCleanup: boolean;\r\n validation: boolean;\r\n documentation: boolean;\r\n templates: boolean;\r\n}\r\n\r\n/**\r\n * Normalise the constructor-supplied option (boolean | object |\r\n * undefined) into a fully-resolved config. Returns `null` when the\r\n * toolbar is disabled (default).\r\n */\r\nexport function resolveControlsConfig(\r\n option: boolean | ControlsConfig | undefined,\r\n): ResolvedControlsConfig | null {\r\n if (option === undefined || option === false) return null;\r\n const cfg = option === true ? {} : option;\r\n return {\r\n position: cfg.position ?? 'top-right',\r\n offset: cfg.offset ?? {},\r\n zoomIn: cfg.zoomIn ?? true,\r\n zoomOut: cfg.zoomOut ?? true,\r\n fit: cfg.fit ?? true,\r\n undo: cfg.undo ?? true,\r\n redo: cfg.redo ?? true,\r\n routeCleanup: cfg.routeCleanup ?? false,\r\n validation: cfg.validation ?? false,\r\n documentation: cfg.documentation ?? false,\r\n templates: cfg.templates ?? false,\r\n };\r\n}\r\n\r\ninterface AttachControlsOptions {\r\n /** Editor host container; the toolbar is appended as a sibling. */\r\n container: HTMLElement;\r\n /** Resolved toolbar config. */\r\n config: ResolvedControlsConfig;\r\n /** Suppress undo/redo when true. */\r\n readonly: boolean;\r\n /** Action callbacks - the facade routes these to the public API. */\r\n onZoomIn: () => void;\r\n onZoomOut: () => void;\r\n onFit: () => void;\r\n onUndo: () => void;\r\n onRedo: () => void;\r\n onRouteCleanup?: () => void;\r\n onValidation?: () => void;\r\n onDocumentation?: () => void | Promise;\r\n onTemplates?: (anchorRect: DOMRect) => void;\r\n /**\r\n * Subscribes the toolbar to canUndo/canRedo state so the buttons\r\n * can disable themselves when their stacks are empty. The callback\r\n * is invoked once immediately with the current state, then on every\r\n * `command-executed` event. Returns the unsubscribe function which\r\n * `dispose()` will call.\r\n */\r\n subscribeUndoRedoState: (\r\n cb: (state: { canUndo: boolean; canRedo: boolean }) => void,\r\n ) => () => void;\r\n /** Optional document override (mirrors RenderOptions.document). */\r\n document?: Document;\r\n /**\r\n * Translator for button tooltips / aria text. Defaults to the\r\n * built-in English translator; the facade supplies one built from\r\n * `BpmnEditorOptions.language`.\r\n */\r\n translate?: Translator;\r\n}\r\n\r\ninterface AttachedControls {\r\n /** Mounted root element. */\r\n element: HTMLElement;\r\n /** Tear-down: removes the element + detaches listeners. */\r\n dispose: () => void;\r\n}\r\n\r\n/**\r\n * Mount the toolbar inside `container`. Idempotent at the call\r\n * site - caller disposes any previous instance before remount.\r\n */\r\nexport function attachControls(\r\n options: AttachControlsOptions,\r\n): AttachedControls {\r\n const doc = options.document ?? options.container.ownerDocument;\r\n if (!doc) {\r\n throw new Error('attachControls: container has no ownerDocument');\r\n }\r\n const t: Translator = options.translate ?? englishTranslator;\r\n\r\n injectControlsStyles(doc);\r\n\r\n const root = doc.createElement('div');\r\n root.className = `mz-controls mz-controls--${options.config.position}`;\r\n applyPositionOffset(root, options.config.offset);\r\n root.setAttribute('data-mz-chrome', 'true');\r\n root.setAttribute('data-testid', 'controls');\r\n root.setAttribute('role', 'toolbar');\r\n root.setAttribute('aria-label', t('toolbar.aria.controls'));\r\n\r\n // Stop pointer events from bubbling to the editor's viewport so the\r\n // pan-drag handler never claims a button click. Same fix the palette\r\n // uses (see palette.ts onPointerDown).\r\n const onPointerDown = (ev: Event): void => {\r\n ev.stopPropagation();\r\n };\r\n root.addEventListener('pointerdown', onPointerDown);\r\n root.addEventListener('mousedown', onPointerDown);\r\n\r\n // Build the button set. Visibility flags + readonly drive which\r\n // buttons render. Order is fixed: undo, redo (history) | zoom out,\r\n // fit, zoom in (view). A separator divides the groups.\r\n const buttons: HTMLButtonElement[] = [];\r\n let undoBtn: HTMLButtonElement | null = null;\r\n let redoBtn: HTMLButtonElement | null = null;\r\n\r\n if (!options.readonly && options.config.undo) {\r\n undoBtn = buildIconButton(doc, {\r\n testid: 'controls-undo',\r\n label: `${t('toolbar.undo')} (Ctrl+Z)`,\r\n svg: ICON_UNDO,\r\n onClick: options.onUndo,\r\n });\r\n buttons.push(undoBtn);\r\n root.appendChild(undoBtn);\r\n }\r\n if (!options.readonly && options.config.redo) {\r\n redoBtn = buildIconButton(doc, {\r\n testid: 'controls-redo',\r\n label: `${t('toolbar.redo')} (Ctrl+Shift+Z)`,\r\n svg: ICON_REDO,\r\n onClick: options.onRedo,\r\n });\r\n buttons.push(redoBtn);\r\n root.appendChild(redoBtn);\r\n }\r\n if (\r\n (undoBtn || redoBtn) &&\r\n (options.config.zoomOut || options.config.fit || options.config.zoomIn)\r\n ) {\r\n const sep = doc.createElement('span');\r\n sep.className = 'mz-controls-separator';\r\n sep.setAttribute('data-mz-chrome', 'true');\r\n sep.setAttribute('aria-hidden', 'true');\r\n root.appendChild(sep);\r\n }\r\n if (options.config.zoomOut) {\r\n const b = buildIconButton(doc, {\r\n testid: 'controls-zoom-out',\r\n label: t('toolbar.zoomOut'),\r\n svg: ICON_ZOOM_OUT,\r\n onClick: options.onZoomOut,\r\n });\r\n buttons.push(b);\r\n root.appendChild(b);\r\n }\r\n if (options.config.fit) {\r\n const b = buildIconButton(doc, {\r\n testid: 'controls-fit',\r\n label: t('toolbar.fit'),\r\n svg: ICON_FIT,\r\n onClick: options.onFit,\r\n });\r\n buttons.push(b);\r\n root.appendChild(b);\r\n }\r\n if (options.config.zoomIn) {\r\n const b = buildIconButton(doc, {\r\n testid: 'controls-zoom-in',\r\n label: t('toolbar.zoomIn'),\r\n svg: ICON_ZOOM_IN,\r\n onClick: options.onZoomIn,\r\n });\r\n buttons.push(b);\r\n root.appendChild(b);\r\n }\r\n\r\n if (\r\n !options.readonly &&\r\n (options.config.routeCleanup || options.config.validation ||\r\n options.config.documentation || options.config.templates)\r\n ) {\r\n const sep = doc.createElement('span');\r\n sep.className = 'mz-controls-separator';\r\n sep.setAttribute('data-mz-chrome', 'true');\r\n sep.setAttribute('aria-hidden', 'true');\r\n root.appendChild(sep);\r\n }\r\n if (!options.readonly && options.config.routeCleanup && options.onRouteCleanup) {\r\n const b = buildIconButton(doc, {\r\n testid: 'controls-route-cleanup',\r\n label: t('toolbar.routeCleanup'),\r\n svg: ICON_ROUTE_CLEANUP,\r\n onClick: options.onRouteCleanup,\r\n });\r\n buttons.push(b);\r\n root.appendChild(b);\r\n }\r\n if (!options.readonly && options.config.validation && options.onValidation) {\r\n const b = buildIconButton(doc, {\r\n testid: 'controls-validation',\r\n label: t('toolbar.validation'),\r\n svg: ICON_VALIDATION,\r\n onClick: options.onValidation,\r\n });\r\n buttons.push(b);\r\n root.appendChild(b);\r\n }\r\n if (!options.readonly && options.config.documentation && options.onDocumentation) {\r\n const b = buildIconButton(doc, {\r\n testid: 'controls-documentation',\r\n label: t('toolbar.documentation'),\r\n svg: ICON_DOCUMENTATION,\r\n onClick: options.onDocumentation,\r\n });\r\n buttons.push(b);\r\n root.appendChild(b);\r\n }\r\n if (!options.readonly && options.config.templates && options.onTemplates) {\r\n const b = buildIconButton(doc, {\r\n testid: 'controls-templates',\r\n label: t('toolbar.templates'),\r\n svg: ICON_TEMPLATES,\r\n onClick: (_ev, button) => {\r\n options.onTemplates?.(button.getBoundingClientRect());\r\n },\r\n });\r\n buttons.push(b);\r\n root.appendChild(b);\r\n }\r\n\r\n // Wire undo/redo enabled state. The facade fires the callback once\r\n // synchronously with the current stack state, then on every\r\n // `command-executed` event.\r\n const unsubState = options.subscribeUndoRedoState(({ canUndo, canRedo }) => {\r\n if (undoBtn) {\r\n undoBtn.disabled = !canUndo;\r\n undoBtn.setAttribute('aria-disabled', String(!canUndo));\r\n }\r\n if (redoBtn) {\r\n redoBtn.disabled = !canRedo;\r\n redoBtn.setAttribute('aria-disabled', String(!canRedo));\r\n }\r\n });\r\n\r\n // Anchor the toolbar inside the container. We use absolute\r\n // positioning rather than ensureContainerLayout (which the palette\r\n // does as a flex sibling) because the toolbar floats over the\r\n // canvas and shouldn't change the canvas's layout flow.\r\n ensureContainerPositioned(options.container);\r\n options.container.appendChild(root);\r\n\r\n return {\r\n element: root,\r\n dispose: (): void => {\r\n unsubState();\r\n root.removeEventListener('pointerdown', onPointerDown);\r\n root.removeEventListener('mousedown', onPointerDown);\r\n if (root.parentNode === options.container) {\r\n options.container.removeChild(root);\r\n }\r\n },\r\n };\r\n}\r\n\r\ninterface ButtonSpec {\r\n testid: string;\r\n label: string;\r\n svg: string;\r\n onClick: (ev: MouseEvent, button: HTMLButtonElement) => void | Promise;\r\n}\r\n\r\nfunction buildIconButton(doc: Document, spec: ButtonSpec): HTMLButtonElement {\r\n const btn = doc.createElement('button');\r\n btn.type = 'button';\r\n btn.className = 'mz-controls-item';\r\n btn.setAttribute('data-mz-chrome', 'true');\r\n btn.setAttribute('data-testid', spec.testid);\r\n btn.title = spec.label;\r\n btn.setAttribute('aria-label', spec.label);\r\n btn.innerHTML = spec.svg;\r\n btn.addEventListener('click', (ev) => {\r\n ev.preventDefault();\r\n ev.stopPropagation();\r\n void spec.onClick(ev, btn);\r\n });\r\n return btn;\r\n}\r\n\r\n/**\r\n * Ensure the editor container can host an absolutely-positioned\r\n * child without stomping any host-set position. Mirrors the safe-on-\r\n * static-only guard the context pad uses.\r\n */\r\nfunction ensureContainerPositioned(container: HTMLElement): void {\r\n const cs = (\r\n container.ownerDocument?.defaultView ?? globalThis\r\n ).getComputedStyle?.(container);\r\n if (cs && cs.position === 'static') {\r\n container.style.position = 'relative';\r\n }\r\n}\r\n\r\nfunction applyPositionOffset(root: HTMLElement, offset: ControlsOffset): void {\r\n for (const edge of ['top', 'right', 'bottom', 'left'] as const) {\r\n const value = offset[edge];\r\n if (typeof value === 'number' && Number.isFinite(value)) {\r\n root.style[edge] = `${value}px`;\r\n }\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Inline SVG icons (mirror Sample/Pages/Editor.razor lines 117-139) */\r\n/* ------------------------------------------------------------------ */\r\n\r\nconst ICON_UNDO =\r\n '';\r\n\r\nconst ICON_REDO =\r\n '';\r\n\r\nconst ICON_ZOOM_OUT =\r\n '';\r\n\r\nconst ICON_FIT =\r\n '';\r\n\r\nconst ICON_ZOOM_IN =\r\n '';\r\n\r\nconst ICON_ROUTE_CLEANUP =\r\n '';\r\n\r\nconst ICON_VALIDATION =\r\n '';\r\n\r\nconst ICON_DOCUMENTATION =\r\n '';\r\n\r\nconst ICON_TEMPLATES =\r\n '';\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* CSS injection - once per document */\r\n/* ------------------------------------------------------------------ */\r\n\r\nconst CONTROLS_STYLE_ID = 'mz-controls-styles';\r\n\r\nfunction injectControlsStyles(doc: Document): void {\r\n if (doc.getElementById(CONTROLS_STYLE_ID)) return;\r\n const style = doc.createElement('style');\r\n style.id = CONTROLS_STYLE_ID;\r\n style.textContent = `\r\n .mz-controls {\r\n position: absolute;\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n gap: 4px;\r\n padding: 4px;\r\n background: var(--mz-color-surface-default, #ffffff);\r\n border: 1px solid var(--mz-color-stroke-muted, #cbd5e1);\r\n border-radius: 6px;\r\n box-shadow: 0 2px 6px rgba(15, 23, 42, 0.10);\r\n z-index: 10;\r\n }\r\n .mz-controls--top-right { top: 16px; right: 16px; }\r\n .mz-controls--top-left { top: 16px; left: 16px; }\r\n .mz-controls--bottom-right { bottom: 16px; right: 16px; }\r\n .mz-controls--bottom-left { bottom: 16px; left: 16px; }\r\n .mz-controls-item {\r\n width: 32px;\r\n height: 32px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 0;\r\n background: transparent;\r\n border: 1px solid transparent;\r\n border-radius: 6px;\r\n color: var(--mz-color-stroke-default, #1f2937);\r\n cursor: pointer;\r\n transition: background-color 0.12s ease, border-color 0.12s ease, opacity 0.12s ease;\r\n }\r\n .mz-controls-item:hover:not(:disabled) {\r\n background: rgba(99, 102, 241, 0.10);\r\n border-color: rgba(99, 102, 241, 0.30);\r\n }\r\n .mz-controls-item:active:not(:disabled) {\r\n background: rgba(99, 102, 241, 0.18);\r\n }\r\n .mz-controls-item:disabled {\r\n opacity: 0.35;\r\n cursor: not-allowed;\r\n }\r\n .mz-controls-item svg { display: block; pointer-events: none; }\r\n .mz-controls-separator {\r\n width: 1px;\r\n height: 20px;\r\n background: var(--mz-color-stroke-muted, #cbd5e1);\r\n margin: 0 2px;\r\n align-self: center;\r\n }\r\n `;\r\n doc.head.appendChild(style);\r\n}\r\n","/**\r\n * mindzie.BpmnEditor - lint message localization\r\n * Copyright (c) mindzie 2026 - proprietary, all rights reserved\r\n *\r\n * Implements: `renderLintMessage`, which turns a `LintIssue`'s\r\n * `messageKey` + raw `messageParams` into a localized string via a\r\n * `Translator`. Lint rules run in the pure model layer without a\r\n * translator, so they emit a stable key plus raw params; this helper\r\n * (called from the UI / facade) resolves the catalog-backed params\r\n * (element type, container word, gateway direction) and interpolates.\r\n *\r\n * Falls back to the issue's English `message` when no `messageKey` is\r\n * present, so a not-yet-converted rule still renders readable text.\r\n *\r\n * License: proprietary (not for redistribution outside mindzie)\r\n */\r\n\r\nimport type { LintIssue } from './types.js';\r\nimport type { MessageParams, Translator } from '../../i18n/types.js';\r\nimport { friendlyType } from './helpers.js';\r\n\r\n/**\r\n * Resolve a raw BPMN element type to its localized display name via the\r\n * `type.*` catalog, falling back to the English de-camelCased form when\r\n * the catalog has no entry for the type.\r\n */\r\nfunction resolveType(t: Translator, raw: string): string {\r\n const key = `type.${raw}`;\r\n const value = t(key);\r\n return value === key ? friendlyType(raw) : value;\r\n}\r\n\r\n/**\r\n * Produce the localized message for a lint issue. Params named `type`,\r\n * `container`, and `direction` are resolved through their catalogs; all\r\n * other params pass through unchanged.\r\n */\r\nexport function renderLintMessage(issue: LintIssue, t: Translator): string {\r\n if (issue.messageKey === undefined) {\r\n return issue.message;\r\n }\r\n const params: Record = { ...(issue.messageParams ?? {}) };\r\n const rawType = params['type'];\r\n if (typeof rawType === 'string') {\r\n params['type'] = resolveType(t, rawType);\r\n }\r\n // `container` is overloaded: some rules pass a container *word*\r\n // ('process' / 'subProcess') to be localized, others pass a container\r\n // *label* (an element name) that must survive verbatim. Only the two\r\n // known word tokens are translated; anything else passes through.\r\n const rawContainer = params['container'];\r\n if (rawContainer === 'process' || rawContainer === 'subProcess') {\r\n params['container'] = t(`lint.containerWord.${rawContainer}`);\r\n }\r\n const rawDirection = params['direction'];\r\n if (typeof rawDirection === 'string') {\r\n params['direction'] = t(`lint.direction.${rawDirection}`);\r\n }\r\n return t(issue.messageKey, params as MessageParams);\r\n}\r\n","'use strict';\n\nconst nameStartChar = ':A-Za-z_\\\\u00C0-\\\\u00D6\\\\u00D8-\\\\u00F6\\\\u00F8-\\\\u02FF\\\\u0370-\\\\u037D\\\\u037F-\\\\u1FFF\\\\u200C-\\\\u200D\\\\u2070-\\\\u218F\\\\u2C00-\\\\u2FEF\\\\u3001-\\\\uD7FF\\\\uF900-\\\\uFDCF\\\\uFDF0-\\\\uFFFD';\nconst nameChar = nameStartChar + '\\\\-.\\\\d\\\\u00B7\\\\u0300-\\\\u036F\\\\u203F-\\\\u2040';\nconst nameRegexp = '[' + nameStartChar + '][' + nameChar + ']*'\nconst regexName = new RegExp('^' + nameRegexp + '$');\n\nconst getAllMatches = function (string, regex) {\n const matches = [];\n let match = regex.exec(string);\n while (match) {\n const allmatches = [];\n allmatches.startIndex = regex.lastIndex - match[0].length;\n const len = match.length;\n for (let index = 0; index < len; index++) {\n allmatches.push(match[index]);\n }\n matches.push(allmatches);\n match = regex.exec(string);\n }\n return matches;\n};\n\nconst isName = function (string) {\n const match = regexName.exec(string);\n return !(match === null || typeof match === 'undefined');\n};\n\nexports.isExist = function (v) {\n return typeof v !== 'undefined';\n};\n\nexports.isEmptyObject = function (obj) {\n return Object.keys(obj).length === 0;\n};\n\n/**\n * Copy all the properties of a into b.\n * @param {*} target\n * @param {*} a\n */\nexports.merge = function (target, a, arrayMode) {\n if (a) {\n const keys = Object.keys(a); // will return an array of own properties\n const len = keys.length; //don't make it inline\n for (let i = 0; i < len; i++) {\n if (arrayMode === 'strict') {\n target[keys[i]] = [a[keys[i]]];\n } else {\n target[keys[i]] = a[keys[i]];\n }\n }\n }\n};\n/* exports.merge =function (b,a){\n return Object.assign(b,a);\n} */\n\nexports.getValue = function (v) {\n if (exports.isExist(v)) {\n return v;\n } else {\n return '';\n }\n};\n\n/**\n * Dangerous property names that could lead to prototype pollution or security issues\n */\nconst DANGEROUS_PROPERTY_NAMES = [\n // '__proto__',\n // 'constructor',\n // 'prototype',\n 'hasOwnProperty',\n 'toString',\n 'valueOf',\n '__defineGetter__',\n '__defineSetter__',\n '__lookupGetter__',\n '__lookupSetter__'\n];\n\nconst criticalProperties = [\"__proto__\", \"constructor\", \"prototype\"];\n\nexports.isName = isName;\nexports.getAllMatches = getAllMatches;\nexports.nameRegexp = nameRegexp;\nexports.DANGEROUS_PROPERTY_NAMES = DANGEROUS_PROPERTY_NAMES;\nexports.criticalProperties = criticalProperties;\n","'use strict';\n\nconst util = require('./util');\n\nconst defaultOptions = {\n allowBooleanAttributes: false, //A tag can have attributes without any value\n unpairedTags: []\n};\n\n//const tagsPattern = new RegExp(\"<\\\\/?([\\\\w:\\\\-_\\.]+)\\\\s*\\/?>\",\"g\");\nexports.validate = function (xmlData, options) {\n options = Object.assign({}, defaultOptions, options);\n\n //xmlData = xmlData.replace(/(\\r\\n|\\n|\\r)/gm,\"\");//make it single line\n //xmlData = xmlData.replace(/(^\\s*<\\?xml.*?\\?>)/g,\"\");//Remove XML starting tag\n //xmlData = xmlData.replace(/()/g,\"\");//Remove DOCTYPE\n const tags = [];\n let tagFound = false;\n\n //indicates that the root tag has been closed (aka. depth 0 has been reached)\n let reachedRoot = false;\n\n if (xmlData[0] === '\\ufeff') {\n // check for byte order mark (BOM)\n xmlData = xmlData.substr(1);\n }\n \n for (let i = 0; i < xmlData.length; i++) {\n\n if (xmlData[i] === '<' && xmlData[i+1] === '?') {\n i+=2;\n i = readPI(xmlData,i);\n if (i.err) return i;\n }else if (xmlData[i] === '<') {\n //starting of tag\n //read until you reach to '>' avoiding any '>' in attribute value\n let tagStartPos = i;\n i++;\n \n if (xmlData[i] === '!') {\n i = readCommentAndCDATA(xmlData, i);\n continue;\n } else {\n let closingTag = false;\n if (xmlData[i] === '/') {\n //closing tag\n closingTag = true;\n i++;\n }\n //read tagname\n let tagName = '';\n for (; i < xmlData.length &&\n xmlData[i] !== '>' &&\n xmlData[i] !== ' ' &&\n xmlData[i] !== '\\t' &&\n xmlData[i] !== '\\n' &&\n xmlData[i] !== '\\r'; i++\n ) {\n tagName += xmlData[i];\n }\n tagName = tagName.trim();\n //console.log(tagName);\n\n if (tagName[tagName.length - 1] === '/') {\n //self closing tag without attributes\n tagName = tagName.substring(0, tagName.length - 1);\n //continue;\n i--;\n }\n if (!validateTagName(tagName)) {\n let msg;\n if (tagName.trim().length === 0) {\n msg = \"Invalid space after '<'.\";\n } else {\n msg = \"Tag '\"+tagName+\"' is an invalid name.\";\n }\n return getErrorObject('InvalidTag', msg, getLineNumberForPosition(xmlData, i));\n }\n\n const result = readAttributeStr(xmlData, i);\n if (result === false) {\n return getErrorObject('InvalidAttr', \"Attributes for '\"+tagName+\"' have open quote.\", getLineNumberForPosition(xmlData, i));\n }\n let attrStr = result.value;\n i = result.index;\n\n if (attrStr[attrStr.length - 1] === '/') {\n //self closing tag\n const attrStrStart = i - attrStr.length;\n attrStr = attrStr.substring(0, attrStr.length - 1);\n const isValid = validateAttributeString(attrStr, options);\n if (isValid === true) {\n tagFound = true;\n //continue; //text may presents after self closing tag\n } else {\n //the result from the nested function returns the position of the error within the attribute\n //in order to get the 'true' error line, we need to calculate the position where the attribute begins (i - attrStr.length) and then add the position within the attribute\n //this gives us the absolute index in the entire xml, which we can use to find the line at last\n return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, attrStrStart + isValid.err.line));\n }\n } else if (closingTag) {\n if (!result.tagClosed) {\n return getErrorObject('InvalidTag', \"Closing tag '\"+tagName+\"' doesn't have proper closing.\", getLineNumberForPosition(xmlData, i));\n } else if (attrStr.trim().length > 0) {\n return getErrorObject('InvalidTag', \"Closing tag '\"+tagName+\"' can't have attributes or invalid starting.\", getLineNumberForPosition(xmlData, tagStartPos));\n } else if (tags.length === 0) {\n return getErrorObject('InvalidTag', \"Closing tag '\"+tagName+\"' has not been opened.\", getLineNumberForPosition(xmlData, tagStartPos));\n } else {\n const otg = tags.pop();\n if (tagName !== otg.tagName) {\n let openPos = getLineNumberForPosition(xmlData, otg.tagStartPos);\n return getErrorObject('InvalidTag',\n \"Expected closing tag '\"+otg.tagName+\"' (opened in line \"+openPos.line+\", col \"+openPos.col+\") instead of closing tag '\"+tagName+\"'.\",\n getLineNumberForPosition(xmlData, tagStartPos));\n }\n\n //when there are no more tags, we reached the root level.\n if (tags.length == 0) {\n reachedRoot = true;\n }\n }\n } else {\n const isValid = validateAttributeString(attrStr, options);\n if (isValid !== true) {\n //the result from the nested function returns the position of the error within the attribute\n //in order to get the 'true' error line, we need to calculate the position where the attribute begins (i - attrStr.length) and then add the position within the attribute\n //this gives us the absolute index in the entire xml, which we can use to find the line at last\n return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, i - attrStr.length + isValid.err.line));\n }\n\n //if the root level has been reached before ...\n if (reachedRoot === true) {\n return getErrorObject('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));\n } else if(options.unpairedTags.indexOf(tagName) !== -1){\n //don't push into stack\n } else {\n tags.push({tagName, tagStartPos});\n }\n tagFound = true;\n }\n\n //skip tag text value\n //It may include comments and CDATA value\n for (i++; i < xmlData.length; i++) {\n if (xmlData[i] === '<') {\n if (xmlData[i + 1] === '!') {\n //comment or CADATA\n i++;\n i = readCommentAndCDATA(xmlData, i);\n continue;\n } else if (xmlData[i+1] === '?') {\n i = readPI(xmlData, ++i);\n if (i.err) return i;\n } else{\n break;\n }\n } else if (xmlData[i] === '&') {\n const afterAmp = validateAmpersand(xmlData, i);\n if (afterAmp == -1)\n return getErrorObject('InvalidChar', \"char '&' is not expected.\", getLineNumberForPosition(xmlData, i));\n i = afterAmp;\n }else{\n if (reachedRoot === true && !isWhiteSpace(xmlData[i])) {\n return getErrorObject('InvalidXml', \"Extra text at the end\", getLineNumberForPosition(xmlData, i));\n }\n }\n } //end of reading tag text value\n if (xmlData[i] === '<') {\n i--;\n }\n }\n } else {\n if ( isWhiteSpace(xmlData[i])) {\n continue;\n }\n return getErrorObject('InvalidChar', \"char '\"+xmlData[i]+\"' is not expected.\", getLineNumberForPosition(xmlData, i));\n }\n }\n\n if (!tagFound) {\n return getErrorObject('InvalidXml', 'Start tag expected.', 1);\n }else if (tags.length == 1) {\n return getErrorObject('InvalidTag', \"Unclosed tag '\"+tags[0].tagName+\"'.\", getLineNumberForPosition(xmlData, tags[0].tagStartPos));\n }else if (tags.length > 0) {\n return getErrorObject('InvalidXml', \"Invalid '\"+\n JSON.stringify(tags.map(t => t.tagName), null, 4).replace(/\\r?\\n/g, '')+\n \"' found.\", {line: 1, col: 1});\n }\n\n return true;\n};\n\nfunction isWhiteSpace(char){\n return char === ' ' || char === '\\t' || char === '\\n' || char === '\\r';\n}\n/**\n * Read Processing insstructions and skip\n * @param {*} xmlData\n * @param {*} i\n */\nfunction readPI(xmlData, i) {\n const start = i;\n for (; i < xmlData.length; i++) {\n if (xmlData[i] == '?' || xmlData[i] == ' ') {\n //tagname\n const tagname = xmlData.substr(start, i - start);\n if (i > 5 && tagname === 'xml') {\n return getErrorObject('InvalidXml', 'XML declaration allowed only at the start of the document.', getLineNumberForPosition(xmlData, i));\n } else if (xmlData[i] == '?' && xmlData[i + 1] == '>') {\n //check if valid attribut string\n i++;\n break;\n } else {\n continue;\n }\n }\n }\n return i;\n}\n\nfunction readCommentAndCDATA(xmlData, i) {\n if (xmlData.length > i + 5 && xmlData[i + 1] === '-' && xmlData[i + 2] === '-') {\n //comment\n for (i += 3; i < xmlData.length; i++) {\n if (xmlData[i] === '-' && xmlData[i + 1] === '-' && xmlData[i + 2] === '>') {\n i += 2;\n break;\n }\n }\n } else if (\n xmlData.length > i + 8 &&\n xmlData[i + 1] === 'D' &&\n xmlData[i + 2] === 'O' &&\n xmlData[i + 3] === 'C' &&\n xmlData[i + 4] === 'T' &&\n xmlData[i + 5] === 'Y' &&\n xmlData[i + 6] === 'P' &&\n xmlData[i + 7] === 'E'\n ) {\n let angleBracketsCount = 1;\n for (i += 8; i < xmlData.length; i++) {\n if (xmlData[i] === '<') {\n angleBracketsCount++;\n } else if (xmlData[i] === '>') {\n angleBracketsCount--;\n if (angleBracketsCount === 0) {\n break;\n }\n }\n }\n } else if (\n xmlData.length > i + 9 &&\n xmlData[i + 1] === '[' &&\n xmlData[i + 2] === 'C' &&\n xmlData[i + 3] === 'D' &&\n xmlData[i + 4] === 'A' &&\n xmlData[i + 5] === 'T' &&\n xmlData[i + 6] === 'A' &&\n xmlData[i + 7] === '['\n ) {\n for (i += 8; i < xmlData.length; i++) {\n if (xmlData[i] === ']' && xmlData[i + 1] === ']' && xmlData[i + 2] === '>') {\n i += 2;\n break;\n }\n }\n }\n\n return i;\n}\n\nconst doubleQuote = '\"';\nconst singleQuote = \"'\";\n\n/**\n * Keep reading xmlData until '<' is found outside the attribute value.\n * @param {string} xmlData\n * @param {number} i\n */\nfunction readAttributeStr(xmlData, i) {\n let attrStr = '';\n let startChar = '';\n let tagClosed = false;\n for (; i < xmlData.length; i++) {\n if (xmlData[i] === doubleQuote || xmlData[i] === singleQuote) {\n if (startChar === '') {\n startChar = xmlData[i];\n } else if (startChar !== xmlData[i]) {\n //if vaue is enclosed with double quote then single quotes are allowed inside the value and vice versa\n } else {\n startChar = '';\n }\n } else if (xmlData[i] === '>') {\n if (startChar === '') {\n tagClosed = true;\n break;\n }\n }\n attrStr += xmlData[i];\n }\n if (startChar !== '') {\n return false;\n }\n\n return {\n value: attrStr,\n index: i,\n tagClosed: tagClosed\n };\n}\n\n/**\n * Select all the attributes whether valid or invalid.\n */\nconst validAttrStrRegxp = new RegExp('(\\\\s*)([^\\\\s=]+)(\\\\s*=)?(\\\\s*([\\'\"])(([\\\\s\\\\S])*?)\\\\5)?', 'g');\n\n//attr, =\"sd\", a=\"amit's\", a=\"sd\"b=\"saf\", ab cd=\"\"\n\nfunction validateAttributeString(attrStr, options) {\n //console.log(\"start:\"+attrStr+\":end\");\n\n //if(attrStr.trim().length === 0) return true; //empty string\n\n const matches = util.getAllMatches(attrStr, validAttrStrRegxp);\n const attrNames = {};\n\n for (let i = 0; i < matches.length; i++) {\n if (matches[i][1].length === 0) {\n //nospace before attribute name: a=\"sd\"b=\"saf\"\n return getErrorObject('InvalidAttr', \"Attribute '\"+matches[i][2]+\"' has no space in starting.\", getPositionFromMatch(matches[i]))\n } else if (matches[i][3] !== undefined && matches[i][4] === undefined) {\n return getErrorObject('InvalidAttr', \"Attribute '\"+matches[i][2]+\"' is without value.\", getPositionFromMatch(matches[i]));\n } else if (matches[i][3] === undefined && !options.allowBooleanAttributes) {\n //independent attribute: ab\n return getErrorObject('InvalidAttr', \"boolean attribute '\"+matches[i][2]+\"' is not allowed.\", getPositionFromMatch(matches[i]));\n }\n /* else if(matches[i][6] === undefined){//attribute without value: ab=\n return { err: { code:\"InvalidAttr\",msg:\"attribute \" + matches[i][2] + \" has no value assigned.\"}};\n } */\n const attrName = matches[i][2];\n if (!validateAttrName(attrName)) {\n return getErrorObject('InvalidAttr', \"Attribute '\"+attrName+\"' is an invalid name.\", getPositionFromMatch(matches[i]));\n }\n if (!attrNames.hasOwnProperty(attrName)) {\n //check for duplicate attribute.\n attrNames[attrName] = 1;\n } else {\n return getErrorObject('InvalidAttr', \"Attribute '\"+attrName+\"' is repeated.\", getPositionFromMatch(matches[i]));\n }\n }\n\n return true;\n}\n\nfunction validateNumberAmpersand(xmlData, i) {\n let re = /\\d/;\n if (xmlData[i] === 'x') {\n i++;\n re = /[\\da-fA-F]/;\n }\n for (; i < xmlData.length; i++) {\n if (xmlData[i] === ';')\n return i;\n if (!xmlData[i].match(re))\n break;\n }\n return -1;\n}\n\nfunction validateAmpersand(xmlData, i) {\n // https://www.w3.org/TR/xml/#dt-charref\n i++;\n if (xmlData[i] === ';')\n return -1;\n if (xmlData[i] === '#') {\n i++;\n return validateNumberAmpersand(xmlData, i);\n }\n let count = 0;\n for (; i < xmlData.length; i++, count++) {\n if (xmlData[i].match(/\\w/) && count < 20)\n continue;\n if (xmlData[i] === ';')\n break;\n return -1;\n }\n return i;\n}\n\nfunction getErrorObject(code, message, lineNumber) {\n return {\n err: {\n code: code,\n msg: message,\n line: lineNumber.line || lineNumber,\n col: lineNumber.col,\n },\n };\n}\n\nfunction validateAttrName(attrName) {\n return util.isName(attrName);\n}\n\n// const startsWithXML = /^xml/i;\n\nfunction validateTagName(tagname) {\n return util.isName(tagname) /* && !tagname.match(startsWithXML) */;\n}\n\n//this function returns the line number for the character at the given index\nfunction getLineNumberForPosition(xmlData, index) {\n const lines = xmlData.substring(0, index).split(/\\r?\\n/);\n return {\n line: lines.length,\n\n // column number is last line's length + 1, because column numbering starts at 1:\n col: lines[lines.length - 1].length + 1\n };\n}\n\n//this function returns the position of the first character of match within attrStr\nfunction getPositionFromMatch(match) {\n return match.startIndex + match[1].length;\n}\n","\nconst { DANGEROUS_PROPERTY_NAMES, criticalProperties } = require(\"../util\");\n\nconst defaultOnDangerousProperty = (name) => {\n if (DANGEROUS_PROPERTY_NAMES.includes(name)) {\n return \"__\" + name;\n }\n return name;\n};\nconst defaultOptions = {\n preserveOrder: false,\n attributeNamePrefix: '@_',\n attributesGroupName: false,\n textNodeName: '#text',\n ignoreAttributes: true,\n removeNSPrefix: false, // remove NS from tag name or attribute name if true\n allowBooleanAttributes: false, //a tag can have attributes without any value\n //ignoreRootElement : false,\n parseTagValue: true,\n parseAttributeValue: false,\n trimValues: true, //Trim string values of tag and attributes\n cdataPropName: false,\n numberParseOptions: {\n hex: true,\n leadingZeros: true,\n eNotation: true\n },\n tagValueProcessor: function (tagName, val) {\n return val;\n },\n attributeValueProcessor: function (attrName, val) {\n return val;\n },\n stopNodes: [], //nested tags will not be parsed even for errors\n alwaysCreateTextNode: false,\n isArray: () => false,\n commentPropName: false,\n unpairedTags: [],\n processEntities: true,\n htmlEntities: false,\n ignoreDeclaration: false,\n ignorePiTags: false,\n transformTagName: false,\n transformAttributeName: false,\n updateTag: function (tagName, jPath, attrs) {\n return tagName\n },\n // skipEmptyListItem: false\n captureMetaData: false,\n maxNestedTags: 100,\n strictReservedNames: true,\n onDangerousProperty: defaultOnDangerousProperty\n};\n/**\n * Validates that a property name is safe to use\n * @param {string} propertyName - The property name to validate\n * @param {string} optionName - The option field name (for error message)\n * @throws {Error} If property name is dangerous\n */\nfunction validatePropertyName(propertyName, optionName) {\n if (typeof propertyName !== 'string') {\n return; // Only validate string property names\n }\n\n const normalized = propertyName.toLowerCase();\n if (DANGEROUS_PROPERTY_NAMES.some(dangerous => normalized === dangerous.toLowerCase())) {\n throw new Error(\n `[SECURITY] Invalid ${optionName}: \"${propertyName}\" is a reserved JavaScript keyword that could cause prototype pollution`\n );\n }\n\n if (criticalProperties.some(dangerous => normalized === dangerous.toLowerCase())) {\n throw new Error(\n `[SECURITY] Invalid ${optionName}: \"${propertyName}\" is a reserved JavaScript keyword that could cause prototype pollution`\n );\n }\n}\n\n/**\n * Normalizes processEntities option for backward compatibility\n * @param {boolean|object} value \n * @returns {object} Always returns normalized object\n */\nfunction normalizeProcessEntities(value) {\n // Boolean backward compatibility\n if (typeof value === 'boolean') {\n return {\n enabled: value, // true or false\n maxEntitySize: 10000,\n maxExpansionDepth: 10,\n maxTotalExpansions: 1000,\n maxExpandedLength: 100000,\n allowedTags: null,\n tagFilter: null\n };\n }\n\n // Object config - merge with defaults\n if (typeof value === 'object' && value !== null) {\n return {\n enabled: value.enabled !== false,\n maxEntitySize: Math.max(1, value.maxEntitySize ?? 10000),\n maxExpansionDepth: Math.max(1, value.maxExpansionDepth ?? 10000),\n maxTotalExpansions: Math.max(1, value.maxTotalExpansions ?? Infinity),\n maxExpandedLength: Math.max(1, value.maxExpandedLength ?? 100000),\n maxEntityCount: Math.max(1, value.maxEntityCount ?? 1000),\n allowedTags: value.allowedTags ?? null,\n tagFilter: value.tagFilter ?? null\n };\n }\n\n // Default to enabled with limits\n return normalizeProcessEntities(true);\n}\n\nconst buildOptions = function (options) {\n const built = Object.assign({}, defaultOptions, options);\n\n\n // Validate property names to prevent prototype pollution\n const propertyNameOptions = [\n { value: built.attributeNamePrefix, name: 'attributeNamePrefix' },\n { value: built.attributesGroupName, name: 'attributesGroupName' },\n { value: built.textNodeName, name: 'textNodeName' },\n { value: built.cdataPropName, name: 'cdataPropName' },\n { value: built.commentPropName, name: 'commentPropName' }\n ];\n\n for (const { value, name } of propertyNameOptions) {\n if (value) {\n validatePropertyName(value, name);\n }\n }\n\n if (built.onDangerousProperty === null) {\n built.onDangerousProperty = defaultOnDangerousProperty;\n }\n\n // Always normalize processEntities for backward compatibility and validation\n built.processEntities = normalizeProcessEntities(built.processEntities);\n //console.debug(built.processEntities)\n return built;\n};\n\nexports.buildOptions = buildOptions;\nexports.defaultOptions = defaultOptions;","'use strict';\n\nclass XmlNode{\n constructor(tagname) {\n this.tagname = tagname;\n this.child = []; //nested tags, text, cdata, comments in order\n this[\":@\"] = {}; //attributes map\n }\n add(key,val){\n // this.child.push( {name : key, val: val, isCdata: isCdata });\n if(key === \"__proto__\") key = \"#__proto__\";\n this.child.push( {[key]: val });\n }\n addChild(node) {\n if(node.tagname === \"__proto__\") node.tagname = \"#__proto__\";\n if(node[\":@\"] && Object.keys(node[\":@\"]).length > 0){\n this.child.push( { [node.tagname]: node.child, [\":@\"]: node[\":@\"] });\n }else{\n this.child.push( { [node.tagname]: node.child });\n }\n };\n};\n\n\nmodule.exports = XmlNode;","const util = require('../util');\n\nclass DocTypeReader {\n constructor(options) {\n this.suppressValidationErr = !options;\n this.options = options || {};\n }\n\n readDocType(xmlData, i) {\n const entities = Object.create(null);\n let entityCount = 0;\n\n if (xmlData[i + 3] === 'O' &&\n xmlData[i + 4] === 'C' &&\n xmlData[i + 5] === 'T' &&\n xmlData[i + 6] === 'Y' &&\n xmlData[i + 7] === 'P' &&\n xmlData[i + 8] === 'E') {\n\n i = i + 9;\n let angleBracketsCount = 1;\n let hasBody = false, comment = false;\n let exp = \"\";\n\n for (; i < xmlData.length; i++) {\n if (xmlData[i] === '<' && !comment) { //Determine the tag type\n if (hasBody && hasSeq(xmlData, \"!ENTITY\", i)) {\n i += 7;\n let entityName, val;\n [entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr);\n if (val.indexOf(\"&\") === -1) { //Parameter entities are not supported\n if (this.options.enabled !== false &&\n this.options.maxEntityCount != null &&\n entityCount >= this.options.maxEntityCount) {\n throw new Error(\n `Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})`\n );\n }\n //const escaped = entityName.replace(/[.\\-+*:]/g, '\\\\.');\n const escaped = entityName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n entities[entityName] = {\n regx: RegExp(`&${escaped};`, \"g\"),\n val: val\n };\n entityCount++;\n }\n } else if (hasBody && hasSeq(xmlData, \"!ELEMENT\", i)) {\n i += 8; //Not supported\n const { index } = this.readElementExp(xmlData, i + 1);\n i = index;\n } else if (hasBody && hasSeq(xmlData, \"!ATTLIST\", i)) {\n i += 8; //Not supported\n // const {index} = this.readAttlistExp(xmlData,i+1);\n // i = index;\n } else if (hasBody && hasSeq(xmlData, \"!NOTATION\", i)) {\n i += 9; //Not supported\n const { index } = this.readNotationExp(xmlData, i + 1, this.suppressValidationErr);\n i = index;\n } else if (hasSeq(xmlData, \"!--\", i)) {\n comment = true;\n } else {\n throw new Error(`Invalid DOCTYPE`);\n }\n\n angleBracketsCount++;\n exp = \"\";\n } else if (xmlData[i] === '>') { //Read tag content\n if (comment) {\n if (xmlData[i - 1] === \"-\" && xmlData[i - 2] === \"-\") {\n comment = false;\n angleBracketsCount--;\n }\n } else {\n angleBracketsCount--;\n }\n if (angleBracketsCount === 0) {\n break;\n }\n } else if (xmlData[i] === '[') {\n hasBody = true;\n } else {\n exp += xmlData[i];\n }\n }\n\n if (angleBracketsCount !== 0) {\n throw new Error(`Unclosed DOCTYPE`);\n }\n } else {\n throw new Error(`Invalid Tag instead of DOCTYPE`);\n }\n\n return { entities, i };\n }\n\n readEntityExp(xmlData, i) {\n //External entities are not supported\n // \n\n //Parameter entities are not supported\n // \n\n //Internal entities are supported\n // \n\n // Skip leading whitespace after this.options.maxEntitySize) {\n throw new Error(\n `Entity \"${entityName}\" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})`\n );\n }\n\n i--;\n return [entityName, entityValue, i];\n }\n\n readNotationExp(xmlData, i) {\n // Skip leading whitespace after \n // \n // \n // \n // \n\n // Skip leading whitespace after {\n while (index < data.length && /\\s/.test(data[index])) {\n index++;\n }\n return index;\n};\n\nfunction hasSeq(data, seq, i) {\n for (let j = 0; j < seq.length; j++) {\n if (seq[j] !== data[i + j + 1]) return false;\n }\n return true;\n}\n\nfunction validateEntityName(name) {\n if (util.isName(name))\n return name;\n else\n throw new Error(`Invalid entity name ${name}`);\n}\n\nmodule.exports = DocTypeReader;","const hexRegex = /^[-+]?0x[a-fA-F0-9]+$/;\nconst numRegex = /^([\\-\\+])?(0*)([0-9]*(\\.[0-9]*)?)$/;\n// const octRegex = /^0x[a-z0-9]+/;\n// const binRegex = /0x[a-z0-9]+/;\n\n \nconst consider = {\n hex : true,\n // oct: false,\n leadingZeros: true,\n decimalPoint: \"\\.\",\n eNotation: true,\n //skipLike: /regex/\n};\n\nfunction toNumber(str, options = {}){\n options = Object.assign({}, consider, options );\n if(!str || typeof str !== \"string\" ) return str;\n \n let trimmedStr = str.trim();\n \n if(options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str;\n else if(str===\"0\") return 0;\n else if (options.hex && hexRegex.test(trimmedStr)) {\n return parse_int(trimmedStr, 16);\n // }else if (options.oct && octRegex.test(str)) {\n // return Number.parseInt(val, 8);\n }else if (trimmedStr.search(/[eE]/)!== -1) { //eNotation\n const notation = trimmedStr.match(/^([-\\+])?(0*)([0-9]*(\\.[0-9]*)?[eE][-\\+]?[0-9]+)$/); \n // +00.123 => [ , '+', '00', '.123', ..\n if(notation){\n // console.log(notation)\n if(options.leadingZeros){ //accept with leading zeros\n trimmedStr = (notation[1] || \"\") + notation[3];\n }else{\n if(notation[2] === \"0\" && notation[3][0]=== \".\"){ //valid number\n }else{\n return str;\n }\n }\n return options.eNotation ? Number(trimmedStr) : str;\n }else{\n return str;\n }\n // }else if (options.parseBin && binRegex.test(str)) {\n // return Number.parseInt(val, 2);\n }else{\n //separate negative sign, leading zeros, and rest number\n const match = numRegex.exec(trimmedStr);\n // +00.123 => [ , '+', '00', '.123', ..\n if(match){\n const sign = match[1];\n const leadingZeros = match[2];\n let numTrimmedByZeros = trimZeros(match[3]); //complete num without leading zeros\n //trim ending zeros for floating number\n \n if(!options.leadingZeros && leadingZeros.length > 0 && sign && trimmedStr[2] !== \".\") return str; //-0123\n else if(!options.leadingZeros && leadingZeros.length > 0 && !sign && trimmedStr[1] !== \".\") return str; //0123\n else if(options.leadingZeros && leadingZeros===str) return 0; //00\n \n else{//no leading zeros or leading zeros are allowed\n const num = Number(trimmedStr);\n const numStr = \"\" + num;\n\n if(numStr.search(/[eE]/) !== -1){ //given number is long and parsed to eNotation\n if(options.eNotation) return num;\n else return str;\n }else if(trimmedStr.indexOf(\".\") !== -1){ //floating number\n if(numStr === \"0\" && (numTrimmedByZeros === \"\") ) return num; //0.0\n else if(numStr === numTrimmedByZeros) return num; //0.456. 0.79000\n else if( sign && numStr === \"-\"+numTrimmedByZeros) return num;\n else return str;\n }\n \n if(leadingZeros){\n return (numTrimmedByZeros === numStr) || (sign+numTrimmedByZeros === numStr) ? num : str\n }else {\n return (trimmedStr === numStr) || (trimmedStr === sign+numStr) ? num : str\n }\n }\n }else{ //non-numeric string\n return str;\n }\n }\n}\n\n/**\n * \n * @param {string} numStr without leading zeros\n * @returns \n */\nfunction trimZeros(numStr){\n if(numStr && numStr.indexOf(\".\") !== -1){//float\n numStr = numStr.replace(/0+$/, \"\"); //remove ending zeros\n if(numStr === \".\") numStr = \"0\";\n else if(numStr[0] === \".\") numStr = \"0\"+numStr;\n else if(numStr[numStr.length-1] === \".\") numStr = numStr.substr(0,numStr.length-1);\n return numStr;\n }\n return numStr;\n}\n\nfunction parse_int(numStr, base){\n //polyfill\n if(parseInt) return parseInt(numStr, base);\n else if(Number.parseInt) return Number.parseInt(numStr, base);\n else if(window && window.parseInt) return window.parseInt(numStr, base);\n else throw new Error(\"parseInt, Number.parseInt, window.parseInt are not supported\")\n}\n\nmodule.exports = toNumber;","function getIgnoreAttributesFn(ignoreAttributes) {\n if (typeof ignoreAttributes === 'function') {\n return ignoreAttributes\n }\n if (Array.isArray(ignoreAttributes)) {\n return (attrName) => {\n for (const pattern of ignoreAttributes) {\n if (typeof pattern === 'string' && attrName === pattern) {\n return true\n }\n if (pattern instanceof RegExp && pattern.test(attrName)) {\n return true\n }\n }\n }\n }\n return () => false\n}\n\nmodule.exports = getIgnoreAttributesFn","'use strict';\n///@ts-check\n\nconst util = require('../util');\nconst xmlNode = require('./xmlNode');\nconst DocTypeReader = require('./DocTypeReader');\nconst toNumber = require(\"strnum\");\nconst getIgnoreAttributesFn = require('../ignoreAttributes')\n\n// const regx =\n// '<((!\\\\[CDATA\\\\[([\\\\s\\\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\\\/)(NAME)\\\\s*>))([^<]*)'\n// .replace(/NAME/g, util.nameRegexp);\n\n//const tagsRegx = new RegExp(\"<(\\\\/?[\\\\w:\\\\-\\._]+)([^>]*)>(\\\\s*\"+cdataRegx+\")*([^<]+)?\",\"g\");\n//const tagsRegx = new RegExp(\"<(\\\\/?)((\\\\w*:)?([\\\\w:\\\\-\\._]+))([^>]*)>([^<]*)(\"+cdataRegx+\"([^<]*))*([^<]+)?\",\"g\");\n\nclass OrderedObjParser {\n constructor(options) {\n this.options = options;\n this.currentNode = null;\n this.tagsNodeStack = [];\n this.docTypeEntities = {};\n this.lastEntities = {\n \"apos\": { regex: /&(apos|#39|#x27);/g, val: \"'\" },\n \"gt\": { regex: /&(gt|#62|#x3E);/g, val: \">\" },\n \"lt\": { regex: /&(lt|#60|#x3C);/g, val: \"<\" },\n \"quot\": { regex: /&(quot|#34|#x22);/g, val: \"\\\"\" },\n };\n this.ampEntity = { regex: /&(amp|#38|#x26);/g, val: \"&\" };\n this.htmlEntities = {\n \"space\": { regex: /&(nbsp|#160);/g, val: \" \" },\n // \"lt\" : { regex: /&(lt|#60);/g, val: \"<\" },\n // \"gt\" : { regex: /&(gt|#62);/g, val: \">\" },\n // \"amp\" : { regex: /&(amp|#38);/g, val: \"&\" },\n // \"quot\" : { regex: /&(quot|#34);/g, val: \"\\\"\" },\n // \"apos\" : { regex: /&(apos|#39);/g, val: \"'\" },\n \"cent\": { regex: /&(cent|#162);/g, val: \"¢\" },\n \"pound\": { regex: /&(pound|#163);/g, val: \"£\" },\n \"yen\": { regex: /&(yen|#165);/g, val: \"¥\" },\n \"euro\": { regex: /&(euro|#8364);/g, val: \"€\" },\n \"copyright\": { regex: /&(copy|#169);/g, val: \"©\" },\n \"reg\": { regex: /&(reg|#174);/g, val: \"®\" },\n \"inr\": { regex: /&(inr|#8377);/g, val: \"₹\" },\n \"num_dec\": { regex: /&#([0-9]{1,7});/g, val: (_, str) => fromCodePoint(str, 10, \"&#\") },\n \"num_hex\": { regex: /&#x([0-9a-fA-F]{1,6});/g, val: (_, str) => fromCodePoint(str, 16, \"&#x\") },\n };\n this.addExternalEntities = addExternalEntities;\n this.parseXml = parseXml;\n this.parseTextData = parseTextData;\n this.resolveNameSpace = resolveNameSpace;\n this.buildAttributesMap = buildAttributesMap;\n this.isItStopNode = isItStopNode;\n this.replaceEntitiesValue = replaceEntitiesValue;\n this.readStopNodeData = readStopNodeData;\n this.saveTextToParentTag = saveTextToParentTag;\n this.addChild = addChild;\n this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes)\n this.entityExpansionCount = 0;\n this.currentExpandedLength = 0;\n\n if (this.options.stopNodes && this.options.stopNodes.length > 0) {\n this.stopNodesExact = new Set();\n this.stopNodesWildcard = new Set();\n for (let i = 0; i < this.options.stopNodes.length; i++) {\n const stopNodeExp = this.options.stopNodes[i];\n if (typeof stopNodeExp !== 'string') continue;\n if (stopNodeExp.startsWith(\"*.\")) {\n this.stopNodesWildcard.add(stopNodeExp.substring(2));\n } else {\n this.stopNodesExact.add(stopNodeExp);\n }\n }\n }\n }\n\n}\n\nfunction addExternalEntities(externalEntities) {\n const entKeys = Object.keys(externalEntities);\n for (let i = 0; i < entKeys.length; i++) {\n const ent = entKeys[i];\n const escaped = ent.replace(/[.\\-+*:]/g, '\\\\.');\n this.lastEntities[ent] = {\n regex: new RegExp(\"&\" + escaped + \";\", \"g\"),\n val: externalEntities[ent]\n }\n }\n}\n\n/**\n * @param {string} val\n * @param {string} tagName\n * @param {string} jPath\n * @param {boolean} dontTrim\n * @param {boolean} hasAttributes\n * @param {boolean} isLeafNode\n * @param {boolean} escapeEntities\n */\nfunction parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) {\n if (val !== undefined) {\n if (this.options.trimValues && !dontTrim) {\n val = val.trim();\n }\n if (val.length > 0) {\n if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath);\n\n const newval = this.options.tagValueProcessor(tagName, val, jPath, hasAttributes, isLeafNode);\n if (newval === null || newval === undefined) {\n //don't parse\n return val;\n } else if (typeof newval !== typeof val || newval !== val) {\n //overwrite\n return newval;\n } else if (this.options.trimValues) {\n return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);\n } else {\n const trimmedVal = val.trim();\n if (trimmedVal === val) {\n return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);\n } else {\n return val;\n }\n }\n }\n }\n}\n\nfunction resolveNameSpace(tagname) {\n if (this.options.removeNSPrefix) {\n const tags = tagname.split(':');\n const prefix = tagname.charAt(0) === '/' ? '/' : '';\n if (tags[0] === 'xmlns') {\n return '';\n }\n if (tags.length === 2) {\n tagname = prefix + tags[1];\n }\n }\n return tagname;\n}\n\n//TODO: change regex to capture NS\n//const attrsRegx = new RegExp(\"([\\\\w\\\\-\\\\.\\\\:]+)\\\\s*=\\\\s*(['\\\"])((.|\\n)*?)\\\\2\",\"gm\");\nconst attrsRegx = new RegExp('([^\\\\s=]+)\\\\s*(=\\\\s*([\\'\"])([\\\\s\\\\S]*?)\\\\3)?', 'gm');\n\nfunction buildAttributesMap(attrStr, jPath, tagName) {\n if (this.options.ignoreAttributes !== true && typeof attrStr === 'string') {\n // attrStr = attrStr.replace(/\\r?\\n/g, ' ');\n //attrStr = attrStr || attrStr.trim();\n\n const matches = util.getAllMatches(attrStr, attrsRegx);\n const len = matches.length; //don't make it inline\n const attrs = {};\n for (let i = 0; i < len; i++) {\n const attrName = this.resolveNameSpace(matches[i][1]);\n if (this.ignoreAttributesFn(attrName, jPath)) {\n continue\n }\n let oldVal = matches[i][4];\n let aName = this.options.attributeNamePrefix + attrName;\n if (attrName.length) {\n if (this.options.transformAttributeName) {\n aName = this.options.transformAttributeName(aName);\n }\n aName = sanitizeName(aName, this.options);\n if (oldVal !== undefined) {\n if (this.options.trimValues) {\n oldVal = oldVal.trim();\n }\n oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath);\n const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPath);\n if (newVal === null || newVal === undefined) {\n //don't parse\n attrs[aName] = oldVal;\n } else if (typeof newVal !== typeof oldVal || newVal !== oldVal) {\n //overwrite\n attrs[aName] = newVal;\n } else {\n //parse\n attrs[aName] = parseValue(\n oldVal,\n this.options.parseAttributeValue,\n this.options.numberParseOptions\n );\n }\n } else if (this.options.allowBooleanAttributes) {\n attrs[aName] = true;\n }\n }\n }\n if (!Object.keys(attrs).length) {\n return;\n }\n if (this.options.attributesGroupName) {\n const attrCollection = {};\n attrCollection[this.options.attributesGroupName] = attrs;\n return attrCollection;\n }\n return attrs\n }\n}\n\nconst parseXml = function (xmlData) {\n xmlData = xmlData.replace(/\\r\\n?/g, \"\\n\"); //TODO: remove this line\n const xmlObj = new xmlNode('!xml');\n let currentNode = xmlObj;\n let textData = \"\";\n let jPath = \"\";\n\n // Reset entity expansion counters for this document\n this.entityExpansionCount = 0;\n this.currentExpandedLength = 0;\n\n const docTypeReader = new DocTypeReader(this.options.processEntities);\n for (let i = 0; i < xmlData.length; i++) {//for each char in XML data\n const ch = xmlData[i];\n if (ch === '<') {\n // const nextIndex = i+1;\n // const _2ndChar = xmlData[nextIndex];\n if (xmlData[i + 1] === '/') {//Closing Tag\n const closeIndex = findClosingIndex(xmlData, \">\", i, \"Closing Tag is not closed.\")\n let tagName = xmlData.substring(i + 2, closeIndex).trim();\n\n if (this.options.removeNSPrefix) {\n const colonIndex = tagName.indexOf(\":\");\n if (colonIndex !== -1) {\n tagName = tagName.substr(colonIndex + 1);\n }\n }\n\n if (this.options.transformTagName) {\n tagName = this.options.transformTagName(tagName);\n }\n\n if (currentNode) {\n textData = this.saveTextToParentTag(textData, currentNode, jPath);\n }\n\n //check if last tag of nested tag was unpaired tag\n const lastTagName = jPath.substring(jPath.lastIndexOf(\".\") + 1);\n if (tagName && this.options.unpairedTags.indexOf(tagName) !== -1) {\n throw new Error(`Unpaired tag can not be used as closing tag: `);\n }\n let propIndex = 0\n if (lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1) {\n propIndex = jPath.lastIndexOf('.', jPath.lastIndexOf('.') - 1)\n this.tagsNodeStack.pop();\n } else {\n propIndex = jPath.lastIndexOf(\".\");\n }\n jPath = jPath.substring(0, propIndex);\n\n currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope\n textData = \"\";\n i = closeIndex;\n } else if (xmlData[i + 1] === '?') {\n\n let tagData = readTagExp(xmlData, i, false, \"?>\");\n if (!tagData) throw new Error(\"Pi Tag is not closed.\");\n\n textData = this.saveTextToParentTag(textData, currentNode, jPath);\n if ((this.options.ignoreDeclaration && tagData.tagName === \"?xml\") || this.options.ignorePiTags) {\n //do nothing\n } else {\n\n const childNode = new xmlNode(tagData.tagName);\n childNode.add(this.options.textNodeName, \"\");\n\n if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {\n childNode[\":@\"] = this.buildAttributesMap(tagData.tagExp, jPath, tagData.tagName);\n }\n this.addChild(currentNode, childNode, jPath, i);\n }\n\n\n i = tagData.closeIndex + 1;\n } else if (xmlData.substr(i + 1, 3) === '!--') {\n const endIndex = findClosingIndex(xmlData, \"-->\", i + 4, \"Comment is not closed.\")\n if (this.options.commentPropName) {\n const comment = xmlData.substring(i + 4, endIndex - 2);\n\n textData = this.saveTextToParentTag(textData, currentNode, jPath);\n\n currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]);\n }\n i = endIndex;\n } else if (xmlData.substr(i + 1, 2) === '!D') {\n const result = docTypeReader.readDocType(xmlData, i);\n this.docTypeEntities = result.entities;\n i = result.i;\n } else if (xmlData.substr(i + 1, 2) === '![') {\n const closeIndex = findClosingIndex(xmlData, \"]]>\", i, \"CDATA is not closed.\") - 2;\n const tagExp = xmlData.substring(i + 9, closeIndex);\n\n textData = this.saveTextToParentTag(textData, currentNode, jPath);\n\n let val = this.parseTextData(tagExp, currentNode.tagname, jPath, true, false, true, true);\n if (val == undefined) val = \"\";\n\n //cdata should be set even if it is 0 length string\n if (this.options.cdataPropName) {\n currentNode.add(this.options.cdataPropName, [{ [this.options.textNodeName]: tagExp }]);\n } else {\n currentNode.add(this.options.textNodeName, val);\n }\n\n i = closeIndex + 2;\n } else {//Opening tag\n let result = readTagExp(xmlData, i, this.options.removeNSPrefix);\n let tagName = result.tagName;\n const rawTagName = result.rawTagName;\n let tagExp = result.tagExp;\n let attrExpPresent = result.attrExpPresent;\n let closeIndex = result.closeIndex;\n\n if (this.options.transformTagName) {\n //console.log(tagExp, tagName)\n const newTagName = this.options.transformTagName(tagName);\n if (tagExp === tagName) {\n tagExp = newTagName\n }\n tagName = newTagName;\n }\n\n if (this.options.strictReservedNames &&\n (tagName === this.options.commentPropName\n || tagName === this.options.cdataPropName\n || tagName === this.options.textNodeName\n || tagName === this.options.attributesGroupName\n )) {\n throw new Error(`Invalid tag name: ${tagName}`);\n }\n\n //save text as child node\n if (currentNode && textData) {\n if (currentNode.tagname !== '!xml') {\n //when nested tag is found\n textData = this.saveTextToParentTag(textData, currentNode, jPath, false);\n }\n }\n\n //check if last tag was unpaired tag\n const lastTag = currentNode;\n if (lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1) {\n currentNode = this.tagsNodeStack.pop();\n jPath = jPath.substring(0, jPath.lastIndexOf(\".\"));\n }\n if (tagName !== xmlObj.tagname) {\n jPath += jPath ? \".\" + tagName : tagName;\n }\n const startIndex = i;\n if (this.isItStopNode(this.stopNodesExact, this.stopNodesWildcard, jPath, tagName)) {\n let tagContent = \"\";\n //self-closing tag\n if (tagExp.length > 0 && tagExp.lastIndexOf(\"/\") === tagExp.length - 1) {\n if (tagName[tagName.length - 1] === \"/\") { //remove trailing '/'\n tagName = tagName.substr(0, tagName.length - 1);\n jPath = jPath.substr(0, jPath.length - 1);\n tagExp = tagName;\n } else {\n tagExp = tagExp.substr(0, tagExp.length - 1);\n }\n i = result.closeIndex;\n }\n //unpaired tag\n else if (this.options.unpairedTags.indexOf(tagName) !== -1) {\n\n i = result.closeIndex;\n }\n //normal tag\n else {\n //read until closing tag is found\n const result = this.readStopNodeData(xmlData, rawTagName, closeIndex + 1);\n if (!result) throw new Error(`Unexpected end of ${rawTagName}`);\n i = result.i;\n tagContent = result.tagContent;\n }\n\n const childNode = new xmlNode(tagName);\n if (tagName !== tagExp && attrExpPresent) {\n childNode[\":@\"] = this.buildAttributesMap(tagExp, jPath, tagName);\n }\n if (tagContent) {\n tagContent = this.parseTextData(tagContent, tagName, jPath, true, attrExpPresent, true, true);\n }\n\n jPath = jPath.substr(0, jPath.lastIndexOf(\".\"));\n childNode.add(this.options.textNodeName, tagContent);\n\n this.addChild(currentNode, childNode, jPath, startIndex);\n } else {\n //selfClosing tag\n if (tagExp.length > 0 && tagExp.lastIndexOf(\"/\") === tagExp.length - 1) {\n if (tagName[tagName.length - 1] === \"/\") { //remove trailing '/'\n tagName = tagName.substr(0, tagName.length - 1);\n jPath = jPath.substr(0, jPath.length - 1);\n tagExp = tagName;\n } else {\n tagExp = tagExp.substr(0, tagExp.length - 1);\n }\n\n if (this.options.transformTagName) {\n const newTagName = this.options.transformTagName(tagName);\n if (tagExp === tagName) {\n tagExp = newTagName\n }\n tagName = newTagName;\n }\n\n const childNode = new xmlNode(tagName);\n if (tagName !== tagExp && attrExpPresent) {\n childNode[\":@\"] = this.buildAttributesMap(tagExp, jPath, tagName);\n }\n this.addChild(currentNode, childNode, jPath, startIndex);\n jPath = jPath.substr(0, jPath.lastIndexOf(\".\"));\n }\n else if (this.options.unpairedTags.indexOf(tagName) !== -1) {//unpaired tag\n const childNode = new xmlNode(tagName);\n if (tagName !== tagExp && attrExpPresent) {\n childNode[\":@\"] = this.buildAttributesMap(tagExp, jPath);\n }\n this.addChild(currentNode, childNode, jPath, startIndex);\n jPath = jPath.substr(0, jPath.lastIndexOf(\".\"));\n i = result.closeIndex;\n // Continue to next iteration without changing currentNode\n continue;\n }\n //opening tag\n else {\n const childNode = new xmlNode(tagName);\n if (this.tagsNodeStack.length > this.options.maxNestedTags) {\n throw new Error(\"Maximum nested tags exceeded\");\n }\n this.tagsNodeStack.push(currentNode);\n\n if (tagName !== tagExp && attrExpPresent) {\n childNode[\":@\"] = this.buildAttributesMap(tagExp, jPath, tagName);\n }\n this.addChild(currentNode, childNode, jPath)\n currentNode = childNode;\n }\n textData = \"\";\n i = closeIndex;\n }\n }\n } else {\n textData += xmlData[i];\n }\n }\n return xmlObj.child;\n}\n\nfunction addChild(currentNode, childNode, jPath, startIndex) {\n // unset startIndex if not requested\n if (!this.options.captureMetaData) startIndex = undefined;\n const result = this.options.updateTag(childNode.tagname, jPath, childNode[\":@\"])\n if (result === false) {\n //do nothing\n } else if (typeof result === \"string\") {\n childNode.tagname = result\n currentNode.addChild(childNode, startIndex);\n } else {\n currentNode.addChild(childNode, startIndex);\n }\n}\n\nconst replaceEntitiesValue = function (val, tagName, jPath) {\n // Performance optimization: Early return if no entities to replace\n if (val.indexOf('&') === -1) {\n return val;\n }\n\n const entityConfig = this.options.processEntities;\n\n if (!entityConfig.enabled) {\n return val;\n }\n\n // Check tag-specific filtering\n if (entityConfig.allowedTags) {\n if (!entityConfig.allowedTags.includes(tagName)) {\n return val; // Skip entity replacement for current tag as not set\n }\n }\n\n if (entityConfig.tagFilter) {\n if (!entityConfig.tagFilter(tagName, jPath)) {\n return val; // Skip based on custom filter\n }\n }\n\n // Replace DOCTYPE entities\n for (let entityName in this.docTypeEntities) {\n const entity = this.docTypeEntities[entityName];\n const matches = val.match(entity.regx);\n\n if (matches) {\n // Track expansions\n this.entityExpansionCount += matches.length;\n\n // Check expansion limit\n if (entityConfig.maxTotalExpansions &&\n this.entityExpansionCount > entityConfig.maxTotalExpansions) {\n throw new Error(\n `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`\n );\n }\n\n // Store length before replacement\n const lengthBefore = val.length;\n val = val.replace(entity.regx, entity.val);\n\n // Check expanded length immediately after replacement\n if (entityConfig.maxExpandedLength) {\n this.currentExpandedLength += (val.length - lengthBefore);\n\n if (this.currentExpandedLength > entityConfig.maxExpandedLength) {\n throw new Error(\n `Total expanded content size exceeded: ${this.currentExpandedLength} > ${entityConfig.maxExpandedLength}`\n );\n }\n }\n }\n }\n if (val.indexOf('&') === -1) return val; // Early exit\n\n // Replace standard entities\n for (const entityName of Object.keys(this.lastEntities)) {\n const entity = this.lastEntities[entityName];\n const matches = val.match(entity.regex);\n if (matches) {\n this.entityExpansionCount += matches.length;\n if (entityConfig.maxTotalExpansions &&\n this.entityExpansionCount > entityConfig.maxTotalExpansions) {\n throw new Error(\n `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`\n );\n }\n }\n val = val.replace(entity.regex, entity.val);\n }\n if (val.indexOf('&') === -1) return val; // Early exit\n\n // Replace HTML entities if enabled\n if (this.options.htmlEntities) {\n for (const entityName of Object.keys(this.htmlEntities)) {\n const entity = this.htmlEntities[entityName];\n const matches = val.match(entity.regex);\n if (matches) {\n //console.log(matches);\n this.entityExpansionCount += matches.length;\n if (entityConfig.maxTotalExpansions &&\n this.entityExpansionCount > entityConfig.maxTotalExpansions) {\n throw new Error(\n `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`\n );\n }\n }\n val = val.replace(entity.regex, entity.val);\n }\n }\n\n // Replace ampersand entity last\n val = val.replace(this.ampEntity.regex, this.ampEntity.val);\n\n return val;\n}\n\nfunction saveTextToParentTag(textData, parentNode, jPath, isLeafNode) {\n if (textData) { //store previously collected data as textNode\n if (isLeafNode === undefined) isLeafNode = parentNode.child.length === 0\n\n textData = this.parseTextData(textData,\n parentNode.tagname,\n jPath,\n false,\n parentNode[\":@\"] ? Object.keys(parentNode[\":@\"]).length !== 0 : false,\n isLeafNode);\n\n if (textData !== undefined && textData !== \"\")\n parentNode.add(this.options.textNodeName, textData);\n textData = \"\";\n }\n return textData;\n}\n\n//TODO: use jPath to simplify the logic\n/**\n * @param {Set} stopNodesExact\n * @param {Set} stopNodesWildcard\n * @param {string} jPath\n * @param {string} currentTagName\n */\nfunction isItStopNode(stopNodesExact, stopNodesWildcard, jPath, currentTagName) {\n if (stopNodesWildcard && stopNodesWildcard.has(currentTagName)) return true;\n if (stopNodesExact && stopNodesExact.has(jPath)) return true;\n return false;\n}\n\n/**\n * Returns the tag Expression and where it is ending handling single-double quotes situation\n * @param {string} xmlData \n * @param {number} i starting index\n * @returns \n */\nfunction tagExpWithClosingIndex(xmlData, i, closingChar = \">\") {\n let attrBoundary;\n let tagExp = \"\";\n for (let index = i; index < xmlData.length; index++) {\n let ch = xmlData[index];\n if (attrBoundary) {\n if (ch === attrBoundary) attrBoundary = \"\";//reset\n } else if (ch === '\"' || ch === \"'\") {\n attrBoundary = ch;\n } else if (ch === closingChar[0]) {\n if (closingChar[1]) {\n if (xmlData[index + 1] === closingChar[1]) {\n return {\n data: tagExp,\n index: index\n }\n }\n } else {\n return {\n data: tagExp,\n index: index\n }\n }\n } else if (ch === '\\t') {\n ch = \" \"\n }\n tagExp += ch;\n }\n}\n\nfunction findClosingIndex(xmlData, str, i, errMsg) {\n const closingIndex = xmlData.indexOf(str, i);\n if (closingIndex === -1) {\n throw new Error(errMsg)\n } else {\n return closingIndex + str.length - 1;\n }\n}\n\nfunction readTagExp(xmlData, i, removeNSPrefix, closingChar = \">\") {\n const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar);\n if (!result) return;\n let tagExp = result.data;\n const closeIndex = result.index;\n const separatorIndex = tagExp.search(/\\s/);\n let tagName = tagExp;\n let attrExpPresent = true;\n if (separatorIndex !== -1) {//separate tag name and attributes expression\n tagName = tagExp.substring(0, separatorIndex);\n tagExp = tagExp.substring(separatorIndex + 1).trimStart();\n }\n\n const rawTagName = tagName;\n if (removeNSPrefix) {\n const colonIndex = tagName.indexOf(\":\");\n if (colonIndex !== -1) {\n tagName = tagName.substr(colonIndex + 1);\n attrExpPresent = tagName !== result.data.substr(colonIndex + 1);\n }\n }\n\n return {\n tagName: tagName,\n tagExp: tagExp,\n closeIndex: closeIndex,\n attrExpPresent: attrExpPresent,\n rawTagName: rawTagName,\n }\n}\n/**\n * find paired tag for a stop node\n * @param {string} xmlData \n * @param {string} tagName \n * @param {number} i \n */\nfunction readStopNodeData(xmlData, tagName, i) {\n const startIndex = i;\n // Starting at 1 since we already have an open tag\n let openTagCount = 1;\n\n for (; i < xmlData.length; i++) {\n if (xmlData[i] === \"<\") {\n if (xmlData[i + 1] === \"/\") {//close tag\n const closeIndex = findClosingIndex(xmlData, \">\", i, `${tagName} is not closed`);\n let closeTagName = xmlData.substring(i + 2, closeIndex).trim();\n if (closeTagName === tagName) {\n openTagCount--;\n if (openTagCount === 0) {\n return {\n tagContent: xmlData.substring(startIndex, i),\n i: closeIndex\n }\n }\n }\n i = closeIndex;\n } else if (xmlData[i + 1] === '?') {\n const closeIndex = findClosingIndex(xmlData, \"?>\", i + 1, \"StopNode is not closed.\")\n i = closeIndex;\n } else if (xmlData.substr(i + 1, 3) === '!--') {\n const closeIndex = findClosingIndex(xmlData, \"-->\", i + 3, \"StopNode is not closed.\")\n i = closeIndex;\n } else if (xmlData.substr(i + 1, 2) === '![') {\n const closeIndex = findClosingIndex(xmlData, \"]]>\", i, \"StopNode is not closed.\") - 2;\n i = closeIndex;\n } else {\n const tagData = readTagExp(xmlData, i, '>')\n\n if (tagData) {\n const openTagName = tagData && tagData.tagName;\n if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length - 1] !== \"/\") {\n openTagCount++;\n }\n i = tagData.closeIndex;\n }\n }\n }\n }//end for loop\n}\n\nfunction parseValue(val, shouldParse, options) {\n if (shouldParse && typeof val === 'string') {\n //console.log(options)\n const newval = val.trim();\n if (newval === 'true') return true;\n else if (newval === 'false') return false;\n else return toNumber(val, options);\n } else {\n if (util.isExist(val)) {\n return val;\n } else {\n return '';\n }\n }\n}\n\nfunction fromCodePoint(str, base, prefix) {\n const codePoint = Number.parseInt(str, base);\n\n if (codePoint >= 0 && codePoint <= 0x10FFFF) {\n return String.fromCodePoint(codePoint);\n } else {\n return prefix + str + \";\";\n }\n}\n\nfunction sanitizeName(name, options) {\n if (util.criticalProperties.includes(name)) {\n throw new Error(`[SECURITY] Invalid name: \"${name}\" is a reserved JavaScript keyword that could cause prototype pollution`);\n } else if (util.DANGEROUS_PROPERTY_NAMES.includes(name)) {\n return options.onDangerousProperty(name);\n }\n return name;\n}\n\nmodule.exports = OrderedObjParser;\n\n","'use strict';\n\n/**\n * \n * @param {array} node \n * @param {any} options \n * @returns \n */\nfunction prettify(node, options){\n return compress( node, options);\n}\n\n/**\n * \n * @param {array} arr \n * @param {object} options \n * @param {string} jPath \n * @returns object\n */\nfunction compress(arr, options, jPath){\n let text;\n const compressedObj = {};\n for (let i = 0; i < arr.length; i++) {\n const tagObj = arr[i];\n const property = propName(tagObj);\n let newJpath = \"\";\n if(jPath === undefined) newJpath = property;\n else newJpath = jPath + \".\" + property;\n\n if(property === options.textNodeName){\n if(text === undefined) text = tagObj[property];\n else text += \"\" + tagObj[property];\n }else if(property === undefined){\n continue;\n }else if(tagObj[property]){\n \n let val = compress(tagObj[property], options, newJpath);\n const isLeaf = isLeafTag(val, options);\n\n if(tagObj[\":@\"]){\n assignAttributes( val, tagObj[\":@\"], newJpath, options);\n }else if(Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode){\n val = val[options.textNodeName];\n }else if(Object.keys(val).length === 0){\n if(options.alwaysCreateTextNode) val[options.textNodeName] = \"\";\n else val = \"\";\n }\n\n if(compressedObj[property] !== undefined && compressedObj.hasOwnProperty(property)) {\n if(!Array.isArray(compressedObj[property])) {\n compressedObj[property] = [ compressedObj[property] ];\n }\n compressedObj[property].push(val);\n }else{\n //TODO: if a node is not an array, then check if it should be an array\n //also determine if it is a leaf node\n if (options.isArray(property, newJpath, isLeaf )) {\n compressedObj[property] = [val];\n }else{\n compressedObj[property] = val;\n }\n }\n }\n \n }\n // if(text && text.length > 0) compressedObj[options.textNodeName] = text;\n if(typeof text === \"string\"){\n if(text.length > 0) compressedObj[options.textNodeName] = text;\n }else if(text !== undefined) compressedObj[options.textNodeName] = text;\n return compressedObj;\n}\n\nfunction propName(obj){\n const keys = Object.keys(obj);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n if(key !== \":@\") return key;\n }\n}\n\nfunction assignAttributes(obj, attrMap, jpath, options){\n if (attrMap) {\n const keys = Object.keys(attrMap);\n const len = keys.length; //don't make it inline\n for (let i = 0; i < len; i++) {\n const atrrName = keys[i];\n if (options.isArray(atrrName, jpath + \".\" + atrrName, true, true)) {\n obj[atrrName] = [ attrMap[atrrName] ];\n } else {\n obj[atrrName] = attrMap[atrrName];\n }\n }\n }\n}\n\nfunction isLeafTag(obj, options){\n const { textNodeName } = options;\n const propCount = Object.keys(obj).length;\n \n if (propCount === 0) {\n return true;\n }\n\n if (\n propCount === 1 &&\n (obj[textNodeName] || typeof obj[textNodeName] === \"boolean\" || obj[textNodeName] === 0)\n ) {\n return true;\n }\n\n return false;\n}\nexports.prettify = prettify;\n","const { buildOptions} = require(\"./OptionsBuilder\");\nconst OrderedObjParser = require(\"./OrderedObjParser\");\nconst { prettify} = require(\"./node2json\");\nconst validator = require('../validator');\n\nclass XMLParser{\n \n constructor(options){\n this.externalEntities = {};\n this.options = buildOptions(options);\n \n }\n /**\n * Parse XML dats to JS object \n * @param {string|Buffer} xmlData \n * @param {boolean|Object} validationOption \n */\n parse(xmlData,validationOption){\n if(typeof xmlData === \"string\"){\n }else if( xmlData.toString){\n xmlData = xmlData.toString();\n }else{\n throw new Error(\"XML data is accepted in String or Bytes[] form.\")\n }\n if( validationOption){\n if(validationOption === true) validationOption = {}; //validate with default options\n \n const result = validator.validate(xmlData, validationOption);\n if (result !== true) {\n throw Error( `${result.err.msg}:${result.err.line}:${result.err.col}` )\n }\n }\n const orderedObjParser = new OrderedObjParser(this.options);\n orderedObjParser.addExternalEntities(this.externalEntities);\n const orderedResult = orderedObjParser.parseXml(xmlData);\n if(this.options.preserveOrder || orderedResult === undefined) return orderedResult;\n else return prettify(orderedResult, this.options);\n }\n\n /**\n * Add Entity which is not by default supported by this library\n * @param {string} key \n * @param {string} value \n */\n addEntity(key, value){\n if(value.indexOf(\"&\") !== -1){\n throw new Error(\"Entity value can't have '&'\")\n }else if(key.indexOf(\"&\") !== -1 || key.indexOf(\";\") !== -1){\n throw new Error(\"An entity must be set without '&' and ';'. Eg. use '#xD' for ' '\")\n }else if(value === \"&\"){\n throw new Error(\"An entity with value '&' is not permitted\");\n }else{\n this.externalEntities[key] = value;\n }\n }\n}\n\nmodule.exports = XMLParser;","const EOL = \"\\n\";\n\n/**\n * \n * @param {array} jArray \n * @param {any} options \n * @returns \n */\nfunction toXml(jArray, options) {\n let indentation = \"\";\n if (options.format && options.indentBy.length > 0) {\n indentation = EOL;\n }\n return arrToStr(jArray, options, \"\", indentation);\n}\n\nfunction arrToStr(arr, options, jPath, indentation) {\n let xmlStr = \"\";\n let isPreviousElementTag = false;\n\n\n if (!Array.isArray(arr)) {\n // Non-array values (e.g. string tag values) should be treated as text content\n if (arr !== undefined && arr !== null) {\n let text = arr.toString();\n text = replaceEntitiesValue(text, options);\n return text;\n }\n return \"\";\n }\n\n for (let i = 0; i < arr.length; i++) {\n const tagObj = arr[i];\n const tagName = propName(tagObj);\n if (tagName === undefined) continue;\n\n let newJPath = \"\";\n if (jPath.length === 0) newJPath = tagName\n else newJPath = `${jPath}.${tagName}`;\n\n if (tagName === options.textNodeName) {\n let tagText = tagObj[tagName];\n if (!isStopNode(newJPath, options)) {\n tagText = options.tagValueProcessor(tagName, tagText);\n tagText = replaceEntitiesValue(tagText, options);\n }\n if (isPreviousElementTag) {\n xmlStr += indentation;\n }\n xmlStr += tagText;\n isPreviousElementTag = false;\n continue;\n } else if (tagName === options.cdataPropName) {\n if (isPreviousElementTag) {\n xmlStr += indentation;\n }\n xmlStr += ``;\n isPreviousElementTag = false;\n continue;\n } else if (tagName === options.commentPropName) {\n xmlStr += indentation + ``;\n isPreviousElementTag = true;\n continue;\n } else if (tagName[0] === \"?\") {\n const attStr = attr_to_str(tagObj[\":@\"], options);\n const tempInd = tagName === \"?xml\" ? \"\" : indentation;\n let piTextNodeName = tagObj[tagName][0][options.textNodeName];\n piTextNodeName = piTextNodeName.length !== 0 ? \" \" + piTextNodeName : \"\"; //remove extra spacing\n xmlStr += tempInd + `<${tagName}${piTextNodeName}${attStr}?>`;\n isPreviousElementTag = true;\n continue;\n }\n let newIdentation = indentation;\n if (newIdentation !== \"\") {\n newIdentation += options.indentBy;\n }\n const attStr = attr_to_str(tagObj[\":@\"], options);\n const tagStart = indentation + `<${tagName}${attStr}`;\n const tagValue = arrToStr(tagObj[tagName], options, newJPath, newIdentation);\n if (options.unpairedTags.indexOf(tagName) !== -1) {\n if (options.suppressUnpairedNode) xmlStr += tagStart + \">\";\n else xmlStr += tagStart + \"/>\";\n } else if ((!tagValue || tagValue.length === 0) && options.suppressEmptyNode) {\n xmlStr += tagStart + \"/>\";\n } else if (tagValue && tagValue.endsWith(\">\")) {\n xmlStr += tagStart + `>${tagValue}${indentation}`;\n } else {\n xmlStr += tagStart + \">\";\n if (tagValue && indentation !== \"\" && (tagValue.includes(\"/>\") || tagValue.includes(\"`;\n }\n isPreviousElementTag = true;\n }\n\n return xmlStr;\n}\n\nfunction propName(obj) {\n const keys = Object.keys(obj);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;\n if (key !== \":@\") return key;\n }\n}\n\nfunction attr_to_str(attrMap, options) {\n let attrStr = \"\";\n if (attrMap && !options.ignoreAttributes) {\n for (let attr in attrMap) {\n if (!Object.prototype.hasOwnProperty.call(attrMap, attr)) continue;\n let attrVal = options.attributeValueProcessor(attr, attrMap[attr]);\n attrVal = replaceEntitiesValue(attrVal, options);\n if (attrVal === true && options.suppressBooleanAttributes) {\n attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}`;\n } else {\n attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}=\"${attrVal}\"`;\n }\n }\n }\n return attrStr;\n}\n\nfunction isStopNode(jPath, options) {\n jPath = jPath.substr(0, jPath.length - options.textNodeName.length - 1);\n let tagName = jPath.substr(jPath.lastIndexOf(\".\") + 1);\n for (let index in options.stopNodes) {\n if (options.stopNodes[index] === jPath || options.stopNodes[index] === \"*.\" + tagName) return true;\n }\n return false;\n}\n\nfunction replaceEntitiesValue(textValue, options) {\n if (textValue && textValue.length > 0 && options.processEntities) {\n for (let i = 0; i < options.entities.length; i++) {\n const entity = options.entities[i];\n textValue = textValue.replace(entity.regex, entity.val);\n }\n }\n return textValue;\n}\nmodule.exports = toXml;\n","'use strict';\n//parse Empty Node as self closing node\nconst buildFromOrderedJs = require('./orderedJs2Xml');\nconst getIgnoreAttributesFn = require('../ignoreAttributes')\n\nconst defaultOptions = {\n attributeNamePrefix: '@_',\n attributesGroupName: false,\n textNodeName: '#text',\n ignoreAttributes: true,\n cdataPropName: false,\n format: false,\n indentBy: ' ',\n suppressEmptyNode: false,\n suppressUnpairedNode: true,\n suppressBooleanAttributes: true,\n tagValueProcessor: function(key, a) {\n return a;\n },\n attributeValueProcessor: function(attrName, a) {\n return a;\n },\n preserveOrder: false,\n commentPropName: false,\n unpairedTags: [],\n entities: [\n { regex: new RegExp(\"&\", \"g\"), val: \"&\" },//it must be on top\n { regex: new RegExp(\">\", \"g\"), val: \">\" },\n { regex: new RegExp(\"<\", \"g\"), val: \"<\" },\n { regex: new RegExp(\"\\'\", \"g\"), val: \"'\" },\n { regex: new RegExp(\"\\\"\", \"g\"), val: \""\" }\n ],\n processEntities: true,\n stopNodes: [],\n // transformTagName: false,\n // transformAttributeName: false,\n oneListGroup: false\n};\n\nfunction Builder(options) {\n this.options = Object.assign({}, defaultOptions, options);\n if (this.options.ignoreAttributes === true || this.options.attributesGroupName) {\n this.isAttribute = function(/*a*/) {\n return false;\n };\n } else {\n this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes)\n this.attrPrefixLen = this.options.attributeNamePrefix.length;\n this.isAttribute = isAttribute;\n }\n\n this.processTextOrObjNode = processTextOrObjNode\n\n if (this.options.format) {\n this.indentate = indentate;\n this.tagEndChar = '>\\n';\n this.newLine = '\\n';\n } else {\n this.indentate = function() {\n return '';\n };\n this.tagEndChar = '>';\n this.newLine = '';\n }\n}\n\nBuilder.prototype.build = function(jObj) {\n if(this.options.preserveOrder){\n return buildFromOrderedJs(jObj, this.options);\n }else {\n if(Array.isArray(jObj) && this.options.arrayNodeName && this.options.arrayNodeName.length > 1){\n jObj = {\n [this.options.arrayNodeName] : jObj\n }\n }\n return this.j2x(jObj, 0, []).val;\n }\n};\n\nBuilder.prototype.j2x = function(jObj, level, ajPath) {\n let attrStr = '';\n let val = '';\n const jPath = ajPath.join('.')\n for (let key in jObj) {\n if(!Object.prototype.hasOwnProperty.call(jObj, key)) continue;\n if (typeof jObj[key] === 'undefined') {\n // supress undefined node only if it is not an attribute\n if (this.isAttribute(key)) {\n val += '';\n }\n } else if (jObj[key] === null) {\n // null attribute should be ignored by the attribute list, but should not cause the tag closing\n if (this.isAttribute(key)) {\n val += '';\n } else if (key === this.options.cdataPropName) {\n val += '';\n } else if (key[0] === '?') {\n val += this.indentate(level) + '<' + key + '?' + this.tagEndChar;\n } else {\n val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;\n }\n // val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;\n } else if (jObj[key] instanceof Date) {\n val += this.buildTextValNode(jObj[key], key, '', level);\n } else if (typeof jObj[key] !== 'object') {\n //premitive type\n const attr = this.isAttribute(key);\n if (attr && !this.ignoreAttributesFn(attr, jPath)) {\n attrStr += this.buildAttrPairStr(attr, '' + jObj[key]);\n } else if (!attr) {\n //tag value\n if (key === this.options.textNodeName) {\n let newval = this.options.tagValueProcessor(key, '' + jObj[key]);\n val += this.replaceEntitiesValue(newval);\n } else {\n val += this.buildTextValNode(jObj[key], key, '', level);\n }\n }\n } else if (Array.isArray(jObj[key])) {\n //repeated nodes\n const arrLen = jObj[key].length;\n let listTagVal = \"\";\n let listTagAttr = \"\";\n for (let j = 0; j < arrLen; j++) {\n const item = jObj[key][j];\n if (typeof item === 'undefined') {\n // supress undefined node\n } else if (item === null) {\n if(key[0] === \"?\") val += this.indentate(level) + '<' + key + '?' + this.tagEndChar;\n else val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;\n // val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;\n } else if (typeof item === 'object') {\n if(this.options.oneListGroup){\n const result = this.j2x(item, level + 1, ajPath.concat(key));\n listTagVal += result.val;\n if (this.options.attributesGroupName && item.hasOwnProperty(this.options.attributesGroupName)) {\n listTagAttr += result.attrStr\n }\n }else{\n listTagVal += this.processTextOrObjNode(item, key, level, ajPath)\n }\n } else {\n if (this.options.oneListGroup) {\n let textValue = this.options.tagValueProcessor(key, item);\n textValue = this.replaceEntitiesValue(textValue);\n listTagVal += textValue;\n } else {\n listTagVal += this.buildTextValNode(item, key, '', level);\n }\n }\n }\n if(this.options.oneListGroup){\n listTagVal = this.buildObjectNode(listTagVal, key, listTagAttr, level);\n }\n val += listTagVal;\n } else {\n //nested node\n if (this.options.attributesGroupName && key === this.options.attributesGroupName) {\n const Ks = Object.keys(jObj[key]);\n const L = Ks.length;\n for (let j = 0; j < L; j++) {\n attrStr += this.buildAttrPairStr(Ks[j], '' + jObj[key][Ks[j]]);\n }\n } else {\n val += this.processTextOrObjNode(jObj[key], key, level, ajPath)\n }\n }\n }\n return {attrStr: attrStr, val: val};\n};\n\nBuilder.prototype.buildAttrPairStr = function(attrName, val){\n val = this.options.attributeValueProcessor(attrName, '' + val);\n val = this.replaceEntitiesValue(val);\n if (this.options.suppressBooleanAttributes && val === \"true\") {\n return ' ' + attrName;\n } else return ' ' + attrName + '=\"' + val + '\"';\n}\n\nfunction processTextOrObjNode (object, key, level, ajPath) {\n const result = this.j2x(object, level + 1, ajPath.concat(key));\n if (object[this.options.textNodeName] !== undefined && Object.keys(object).length === 1) {\n return this.buildTextValNode(object[this.options.textNodeName], key, result.attrStr, level);\n } else {\n return this.buildObjectNode(result.val, key, result.attrStr, level);\n }\n}\n\nBuilder.prototype.buildObjectNode = function(val, key, attrStr, level) {\n if(val === \"\"){\n if(key[0] === \"?\") return this.indentate(level) + '<' + key + attrStr+ '?' + this.tagEndChar;\n else {\n return this.indentate(level) + '<' + key + attrStr + this.closeTag(key) + this.tagEndChar;\n }\n }else{\n\n let tagEndExp = '' + val + tagEndExp );\n } else if (this.options.commentPropName !== false && key === this.options.commentPropName && piClosingChar.length === 0) {\n return this.indentate(level) + `` + this.newLine;\n }else {\n return (\n this.indentate(level) + '<' + key + attrStr + piClosingChar + this.tagEndChar +\n val +\n this.indentate(level) + tagEndExp );\n }\n }\n}\n\nBuilder.prototype.closeTag = function(key){\n let closeTag = \"\";\n if(this.options.unpairedTags.indexOf(key) !== -1){ //unpaired\n if(!this.options.suppressUnpairedNode) closeTag = \"/\"\n }else if(this.options.suppressEmptyNode){ //empty\n closeTag = \"/\";\n }else{\n closeTag = `>` + this.newLine;\n }else if (this.options.commentPropName !== false && key === this.options.commentPropName) {\n return this.indentate(level) + `` + this.newLine;\n }else if(key[0] === \"?\") {//PI tag\n return this.indentate(level) + '<' + key + attrStr+ '?' + this.tagEndChar; \n }else{\n let textValue = this.options.tagValueProcessor(key, val);\n textValue = this.replaceEntitiesValue(textValue);\n \n if( textValue === ''){\n return this.indentate(level) + '<' + key + attrStr + this.closeTag(key) + this.tagEndChar;\n }else{\n return this.indentate(level) + '<' + key + attrStr + '>' +\n textValue +\n ' 0 && this.options.processEntities){\n for (let i=0; i`\r\n * (or its semantic-aliased equivalent).\r\n */\r\nexport function parseBpmn(xml: string): ParseResult {\r\n if (typeof xml !== 'string' || xml.length === 0) {\r\n throw BpmnEditorError.invalidXml(\r\n 'The diagram is empty: no XML content was supplied.',\r\n );\r\n }\r\n\r\n // Strip a UTF-8 BOM if present (research-notes §7).\r\n let xmlText = xml;\r\n if (xmlText.charCodeAt(0) === 0xfeff) {\r\n xmlText = xmlText.slice(1);\r\n }\r\n\r\n // A document of nothing but whitespace is empty in every sense that\r\n // matters, but it is not length 0, so it used to reach the tokenizer and\r\n // come back as a third-party type error (Bug #8272).\r\n if (xmlText.trim().length === 0) {\r\n throw BpmnEditorError.invalidXml(\r\n 'The diagram is empty: the stored document contains only whitespace.',\r\n );\r\n }\r\n\r\n const fxpOptions: Partial = {\r\n ignoreAttributes: false,\r\n attributeNamePrefix: '',\r\n parseAttributeValue: false,\r\n parseTagValue: false,\r\n // Process XML entities, but bump the per-document expansion ceiling\r\n // well above fast-xml-parser's 1000 default — large vendor exports\r\n // (e.g. MID Innovator MIWG fixtures) routinely use 1000-1100 entity\r\n // references for legitimate display labels. The other ceilings\r\n // (`maxEntitySize`, `maxExpansionDepth`, `maxExpandedLength`) keep\r\n // billion-laughs and similar entity-bomb attacks bounded.\r\n processEntities: {\r\n enabled: true,\r\n maxTotalExpansions: 100_000,\r\n } as unknown as boolean,\r\n // htmlEntities unlocks numeric character reference decoding in both\r\n // text and attribute values („ „ etc.). Without this\r\n // fast-xml-parser only decodes the five named XML entities and leaves\r\n // numeric refs as literal text — which surfaces in labels like\r\n // `„where is my pizza?“` from the pizza-collaboration\r\n // fixture rendering as raw \"„\" in the SVG.\r\n htmlEntities: true,\r\n allowBooleanAttributes: true,\r\n preserveOrder: true,\r\n trimValues: false,\r\n // Keep namespace prefixes intact — we resolve them ourselves.\r\n ignoreDeclaration: false,\r\n ignorePiTags: false,\r\n removeNSPrefix: false,\r\n };\r\n\r\n // PARSE FIRST. The reader decides what can be read (Bug #8272).\r\n //\r\n // An earlier cut of this fix ran `XMLValidator` in FRONT of the tokenizer.\r\n // It is not the same reader: fast-xml-parser 4.5.6's validator accepts\r\n // only `\\w` in an entity reference and at most 20 characters, so a\r\n // perfectly valid document declaring `` -\r\n // which the tokenizer parses and expands correctly - was told it was not\r\n // well-formed XML. Refusing a customer's valid diagram is a worse defect\r\n // than the unhelpful message this bug is about, so the validator is now\r\n // only ever asked to EXPLAIN a failure the tokenizer already had.\r\n //\r\n // Nothing here claims the document is well-formed. It claims only that\r\n // the tokenizer could read it - which is the property that matters,\r\n // because the tokenizer is what the rest of this file consumes.\r\n let parsed: PreservedNode[];\r\n try {\r\n const parser = new XMLParser(fxpOptions as X2jOptions);\r\n parsed = parser.parse(xmlText) as PreservedNode[];\r\n } catch (cause) {\r\n throw explainTokenizerFailure(xmlText, cause);\r\n }\r\n\r\n if (!Array.isArray(parsed)) {\r\n throw BpmnEditorError.invalidXml(\r\n 'The diagram could not be read: the document has no XML root element.',\r\n );\r\n }\r\n\r\n // Locate at the top level. Skip processing instructions and\r\n // text nodes (they live as siblings in preserveOrder mode).\r\n const docRoot = findDefinitionsNode(parsed);\r\n if (docRoot === undefined) {\r\n // Name what WAS found. \"Root element is not \" left the\r\n // reader to go and look; the actual root is the whole diagnosis when the\r\n // stored document turns out to be an HTML error page, a JSON payload, or\r\n // somebody else's XML dialect (Bug #8272).\r\n const actualRoot = firstElementName(parsed);\r\n throw BpmnEditorError.unsupportedVersion(\r\n actualRoot === undefined\r\n ? 'This is not a BPMN 2.0 file: the document contains no elements.'\r\n : `This is not a BPMN 2.0 file: its root element is <${actualRoot}>, ` +\r\n `expected .`,\r\n );\r\n }\r\n\r\n const namespaces = extractNamespaceBindings(docRoot);\r\n const definitionsUri = resolveTagNamespace(docRoot.rawName, namespaces);\r\n if (!isModelNamespace(definitionsUri)) {\r\n throw BpmnEditorError.unsupportedVersion(\r\n ` root is in namespace '${definitionsUri ?? '(none)'}'; expected the BPMN 2.0 MODEL namespace.`,\r\n );\r\n }\r\n\r\n const ctx = createContext(namespaces);\r\n const document = createEmptyDocument();\r\n document.namespaces = namespaces;\r\n\r\n // Capture top-level PIs / comments preserved by fast-xml-parser when\r\n // `ignorePiTags: false`. These appear as siblings of .\r\n for (let index = 0; index < parsed.length; index += 1) {\r\n const sibling = parsed[index];\r\n if (sibling === undefined) continue;\r\n const tag = singleKey(sibling);\r\n if (tag === undefined) continue;\r\n if (tag === '?xml' || tag.startsWith('?')) {\r\n const innerArr = sibling[tag];\r\n const text = Array.isArray(innerArr) ? extractText(innerArr) : '';\r\n document.topLevelNodes.push({\r\n kind: 'pi',\r\n target: tag.replace(/^\\?/, ''),\r\n data: text ?? '',\r\n beforeRootIndex: index,\r\n });\r\n } else if (tag === '#comment') {\r\n const innerArr = sibling[tag];\r\n const text = Array.isArray(innerArr) ? extractText(innerArr) : '';\r\n document.topLevelNodes.push({\r\n kind: 'comment',\r\n data: text ?? '',\r\n beforeRootIndex: index,\r\n });\r\n }\r\n }\r\n\r\n // Process attributes.\r\n splitAttributes(docRoot.attrs, ctx, namespaces, (knownLocal, value) => {\r\n document.definitionsAttributes[knownLocal] = value;\r\n }, (foreign) => {\r\n document.definitionsForeignAttributes.push(foreign);\r\n });\r\n\r\n // Walk children of . Resolves each child's tag namespace\r\n // using the child's own xmlns scope so exporters that rebind the\r\n // default namespace directly on `` (Bizagi Modeler) are\r\n // recognised as DI content instead of being mistaken for MODEL.\r\n for (const child of docRoot.children) {\r\n const ns = resolveChildTagNamespace(child, namespaces);\r\n const local = localName(child.rawName);\r\n\r\n if (ns === BPMN_NS.BPMNDI && local === 'BPMNDiagram') {\r\n const diagram = parseBpmnDiagram(child, ctx);\r\n document.diagrams.push(diagram);\r\n indexElement(document, diagram);\r\n document.bodyOrder.push({ kind: 'BPMNDiagram', id: diagram.id });\r\n continue;\r\n }\r\n\r\n if (ns !== undefined && isModelNamespace(ns)) {\r\n // Top-level BPMN MODEL element.\r\n switch (local) {\r\n case 'process': {\r\n const proc = parseProcess(child, ctx);\r\n document.rootElements.push(proc);\r\n indexElement(document, proc);\r\n document.bodyOrder.push({ kind: 'process', id: proc.id });\r\n break;\r\n }\r\n case 'collaboration': {\r\n const col = parseCollaboration(child, ctx);\r\n document.rootElements.push(col);\r\n indexElement(document, col);\r\n document.bodyOrder.push({ kind: 'collaboration', id: col.id });\r\n break;\r\n }\r\n case 'message': {\r\n const msg = parseMessage(child, ctx);\r\n document.rootElements.push(msg);\r\n indexElement(document, msg);\r\n document.bodyOrder.push({ kind: 'message', id: msg.id });\r\n break;\r\n }\r\n case 'error': {\r\n const err = parseError(child, ctx);\r\n document.rootElements.push(err);\r\n indexElement(document, err);\r\n document.bodyOrder.push({ kind: 'error', id: err.id });\r\n break;\r\n }\r\n case 'signal': {\r\n const sig = parseSignal(child, ctx);\r\n document.rootElements.push(sig);\r\n indexElement(document, sig);\r\n document.bodyOrder.push({ kind: 'signal', id: sig.id });\r\n break;\r\n }\r\n case 'escalation': {\r\n const esc = parseEscalation(child, ctx);\r\n document.rootElements.push(esc);\r\n indexElement(document, esc);\r\n document.bodyOrder.push({ kind: 'escalation', id: esc.id });\r\n break;\r\n }\r\n case 'dataStore': {\r\n const ds = parseDataStore(child, ctx);\r\n document.rootElements.push(ds);\r\n indexElement(document, ds);\r\n document.bodyOrder.push({ kind: 'dataStore', id: ds.id });\r\n break;\r\n }\r\n case 'itemDefinition': {\r\n const it = parseItemDefinition(child, ctx);\r\n document.rootElements.push(it);\r\n indexElement(document, it);\r\n document.bodyOrder.push({ kind: 'itemDefinition', id: it.id });\r\n break;\r\n }\r\n case 'category': {\r\n const cat = parseCategory(child, ctx);\r\n document.rootElements.push(cat);\r\n indexElement(document, cat);\r\n document.bodyOrder.push({ kind: 'category', id: cat.id });\r\n break;\r\n }\r\n case 'extensionElements': {\r\n for (const ext of child.children) {\r\n const node = nodeToForeign(ext, namespaces);\r\n if (node !== undefined) document.definitionsExtensionElements.push(node);\r\n }\r\n document.bodyOrder.push({ kind: 'extensionElements' });\r\n break;\r\n }\r\n case 'documentation': {\r\n // Ignore at root level (BPMN spec allows it but it's rarely used).\r\n document.bodyOrder.push({ kind: 'documentation' });\r\n break;\r\n }\r\n default: {\r\n // Unknown BPMN-namespace top-level element. Preserve as a generic\r\n // element so round-trip is lossless.\r\n const generic = parseGenericContainer(child, ctx, local);\r\n document.rootElements.push(generic);\r\n if (generic.id.length > 0) indexElement(document, generic);\r\n document.bodyOrder.push({ kind: local, id: generic.id });\r\n break;\r\n }\r\n }\r\n continue;\r\n }\r\n\r\n // Foreign-namespace child of : capture as foreign.\r\n const foreign = nodeToForeign(child, namespaces);\r\n if (foreign !== undefined) {\r\n document.definitionsExtensionElements.push(foreign);\r\n document.bodyOrder.push({ kind: foreign.localName });\r\n }\r\n }\r\n\r\n document.hasDi = document.diagrams.length > 0;\r\n\r\n // Walk every container and add nested elements to byId so renderer/\r\n // validator can resolve any IDREF — top-level indexing only covers\r\n // the rootElements themselves (research-notes §3).\r\n buildDeepIdIndex(document);\r\n\r\n // Reverse-index lanes → flow nodes (research-notes §2).\r\n applyLaneAssignment(document, ctx);\r\n\r\n // Mark every Tier D element so renderer / API consumers can\r\n // distinguish \"rendered at Fidelity 2\" from \"rendered as placeholder\"\r\n // by reading `element.supported` (BLOCKERS.md Phase-1 P1-AC-09).\r\n applyTierDSupportFlag(document);\r\n\r\n // Report DI that points at an element the document does not contain.\r\n reportDanglingDiReferences(document, docRoot, namespaces, ctx);\r\n\r\n return { document, warnings: ctx.warnings };\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Dangling DI-reference pass */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Reports every `BPMNEdge` whose `bpmnElement` names an id that appears\r\n * NOWHERE in the document, and CHANGES NOTHING (#8267).\r\n *\r\n * This is the residue of #8267 in files already written by a build that\r\n * had the bug: the event-owned data association was deleted from the\r\n * semantic body, and its `BPMNEdge` stayed behind in the plane pointing\r\n * at an id that no longer exists. The fix upstream stops new damage; it\r\n * cannot put back what a previous save already destroyed. Such a file\r\n * used to load, render the arrow, and report `ok` with no warnings, so\r\n * nothing anywhere told the owner the file had been damaged.\r\n *\r\n * KEEP AND REPORT, rather than drop the edge, for three reasons:\r\n *\r\n * 1. The cure for silent data loss cannot be more silent data loss.\r\n * Deleting the edge on load would destroy it on the next save, in\r\n * the same unannounced way that caused the bug.\r\n * 2. The orphan is the only surviving evidence. Its id and waypoints\r\n * record that an association existed and where it was drawn, which\r\n * is what makes the damage diagnosable - and repairable by hand or\r\n * by a later tool. Dropping it discards the last trace.\r\n * 3. Not resolving an id is not proof that the target never existed.\r\n * Destroying data on an inference that cannot be fully verified is\r\n * the wrong trade, and this is a warning precisely because the\r\n * parser is not certain enough to act.\r\n *\r\n * The warning names the ids, because \"this diagram has a problem\" is not\r\n * actionable and \"edge X refers to missing Y\" is.\r\n *\r\n * WHY THIS READS THE RAW TREE AND NOT `document.byId`. Asking byId was\r\n * the obvious implementation and it was WRONG - measurably so. byId is\r\n * built by `buildDeepIdIndex`, which descends only the container slots\r\n * the model types out; anything held as a generic container is not\r\n * indexed. Chief among those is ``, whose `dataInput` /\r\n * `dataOutput` children are real elements with real DI. Against the\r\n * 873-file MIWG / bpmn-io / camunda corpus, the byId version of this\r\n * check fired 1329 times across 115 files, and the great majority were\r\n * ioSpecification members that were present in the file all along. A\r\n * warning that says \"not present in this document\" when it means \"not in\r\n * my index\" is not a warning, it is a lie at scale. Walking the raw\r\n * MODEL-namespace subtree for every `id` attribute answers the question\r\n * that was actually asked, and is complete by construction rather than\r\n * by keeping a list of container types up to date.\r\n *\r\n * Scope, stated rather than implied:\r\n *\r\n * - EDGES only. A dangling `BPMNShape` is a different defect with\r\n * different causes and is not what #8267 produces; reporting it here\r\n * would fold an unmeasured population into a bug that is about\r\n * connectors. It is left alone deliberately, not overlooked.\r\n * - A `bpmnElement` that is absent or empty is NOT reported. It\r\n * references nothing, so nothing about it dangles.\r\n */\r\nfunction reportDanglingDiReferences(\r\n document: BpmnDocument,\r\n docRoot: Element,\r\n namespaces: NamespaceBinding[],\r\n ctx: ParserContext,\r\n): void {\r\n const semanticIds = collectSemanticIds(docRoot, namespaces);\r\n // Per DIAGRAM, not per document. The message says \"in this diagram\",\r\n // so the count has to mean that: accumulating across every plane and\r\n // then slicing once would report a whole document's total under a\r\n // per-diagram sentence, and a multi-diagram file would silently lose\r\n // the naming budget for its later planes to its first one.\r\n for (const diagramElement of document.diagrams) {\r\n const plane = (diagramElement as BpmnDiagram).plane;\r\n if (plane === undefined) continue;\r\n const dangling: { edgeId: string; ref: string }[] = [];\r\n for (const planeElement of plane.planeElements) {\r\n if (planeElement.type !== 'BPMNEdge') continue;\r\n const ref = planeElement.bpmnElement;\r\n if (ref.length === 0) continue;\r\n if (semanticIds.has(bpmnElementLocalPart(ref))) continue;\r\n dangling.push({ edgeId: planeElement.id, ref });\r\n }\r\n\r\n for (const entry of dangling.slice(0, DANGLING_DI_DETAIL_LIMIT)) {\r\n ctx.warnings.push(\r\n ` refers to bpmnElement='${entry.ref}', which is not `\r\n + `present in this document. The connector is being kept as-is; the element it `\r\n + `was drawn for is missing.`,\r\n );\r\n }\r\n const undetailed = dangling.length - DANGLING_DI_DETAIL_LIMIT;\r\n if (undetailed > 0) {\r\n ctx.warnings.push(\r\n `${undetailed} further connector${undetailed === 1 ? '' : 's'} in this diagram also `\r\n + `refer to elements that are not present in the document. All are being kept `\r\n + `as-is.`,\r\n );\r\n }\r\n }\r\n}\r\n\r\n\r\n/**\r\n * How many dangling connectors get named individually before the rest are\r\n * counted in one line.\r\n *\r\n * Applies PER DIAGRAM, matching the wording of the summary message.\r\n *\r\n * Every warning goes to the on-canvas overlay verbatim, one line each, and\r\n * the overlay does not bound the list. A #8267 casualty carries one or two\r\n * orphans and wants naming; a badly damaged plane can carry dozens, and\r\n * naming those would bury every other warning under a wall of ids. The\r\n * remainder is COUNTED rather than dropped - a cap nobody is told about is\r\n * how a report starts lying about coverage.\r\n */\r\nconst DANGLING_DI_DETAIL_LIMIT = 3;\r\n\r\n/**\r\n * Every `id` attribute on a BPMN MODEL element anywhere under\r\n * ``, at any depth, however the model chose to represent\r\n * that element.\r\n *\r\n * DI subtrees are skipped: a `BPMNEdge`'s own id is not something a\r\n * `bpmnElement` is allowed to point at, and including them would let a\r\n * self-referential edge mask its own dangling reference.\r\n */\r\nfunction collectSemanticIds(docRoot: Element, namespaces: NamespaceBinding[]): Set {\r\n const ids = new Set();\r\n const visit = (elem: Element): void => {\r\n const ns = resolveChildTagNamespace(elem, namespaces);\r\n if (ns === BPMN_NS.BPMNDI || ns === BPMN_NS.DC || ns === BPMN_NS.DI) return;\r\n for (const [key, value] of Object.entries(elem.attrs)) {\r\n if (localName(key) === 'id' && value.length > 0) ids.add(value);\r\n }\r\n for (const child of elem.children) visit(child);\r\n };\r\n for (const child of docRoot.children) visit(child);\r\n return ids;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Tier D support-flag pass */\r\n/* ------------------------------------------------------------------ */\r\n\r\nfunction applyTierDSupportFlag(document: BpmnDocument): void {\r\n for (const element of document.byId.values()) {\r\n if (isTierD(element)) {\r\n element.supported = false;\r\n }\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Deep id-index pass */\r\n/* ------------------------------------------------------------------ */\r\n\r\nfunction buildDeepIdIndex(document: BpmnDocument): void {\r\n for (const root of document.rootElements) {\r\n indexDeep(root, document);\r\n }\r\n for (const diagram of document.diagrams) {\r\n indexDeep(diagram, document);\r\n }\r\n}\r\n\r\nfunction indexDeep(elem: BpmnElement, document: BpmnDocument): void {\r\n if (elem.id.length > 0 && !document.byId.has(elem.id)) {\r\n document.byId.set(elem.id, elem);\r\n }\r\n for (const child of getChildren(elem)) {\r\n indexDeep(child, document);\r\n }\r\n}\r\n\r\nfunction getChildren(elem: BpmnElement): BpmnElement[] {\r\n const out: BpmnElement[] = [];\r\n switch (elem.type) {\r\n case 'process': {\r\n const p = elem as Process;\r\n if (p.flowElements !== undefined) out.push(...p.flowElements);\r\n if (p.artifacts !== undefined) out.push(...p.artifacts);\r\n if (p.laneSets !== undefined) out.push(...p.laneSets);\r\n break;\r\n }\r\n case 'subProcess':\r\n case 'transaction':\r\n case 'adHocSubProcess': {\r\n const sp = elem as { flowElements?: BpmnElement[]; artifacts?: BpmnElement[]; laneSets?: BpmnElement[] };\r\n if (sp.flowElements !== undefined) out.push(...sp.flowElements);\r\n if (sp.artifacts !== undefined) out.push(...sp.artifacts);\r\n if (sp.laneSets !== undefined) out.push(...sp.laneSets);\r\n break;\r\n }\r\n case 'collaboration': {\r\n const c = elem as Collaboration;\r\n if (c.participants !== undefined) out.push(...c.participants);\r\n if (c.messageFlows !== undefined) out.push(...c.messageFlows);\r\n if (c.artifacts !== undefined) out.push(...c.artifacts);\r\n break;\r\n }\r\n case 'laneSet': {\r\n const ls = elem as LaneSet;\r\n if (ls.lanes !== undefined) out.push(...ls.lanes);\r\n break;\r\n }\r\n case 'lane': {\r\n const lane = elem as Lane;\r\n if (lane.childLaneSet !== undefined) out.push(lane.childLaneSet);\r\n break;\r\n }\r\n case 'category': {\r\n const cat = elem as Category;\r\n if (cat.categoryValues !== undefined) out.push(...cat.categoryValues);\r\n break;\r\n }\r\n case 'BPMNDiagram': {\r\n const d = elem as BpmnDiagram;\r\n if (d.plane !== undefined) out.push(d.plane);\r\n if (d.labelStyles !== undefined) out.push(...d.labelStyles);\r\n break;\r\n }\r\n case 'BPMNPlane': {\r\n const plane = elem as BpmnPlane;\r\n out.push(...plane.planeElements);\r\n break;\r\n }\r\n case 'BPMNShape': {\r\n const shape = elem as BpmnShape;\r\n if (shape.label !== undefined) out.push(shape.label);\r\n break;\r\n }\r\n case 'BPMNEdge': {\r\n const edge = elem as BpmnEdge;\r\n if (edge.label !== undefined) out.push(edge.label);\r\n break;\r\n }\r\n default:\r\n break;\r\n }\r\n appendDataIoChildren(elem, out);\r\n return out;\r\n}\r\n\r\n/**\r\n * Appends an element's data input/output children to a child walk, for\r\n * every owner type (#8267).\r\n *\r\n * Deliberately NOT a `case` in the switch above. Activities and all five\r\n * event types own these slots under the same property names, so a single\r\n * duck-typed append covers every owner there is - and every owner added\r\n * later - whereas a per-type case is a list that has to be remembered.\r\n * #8267 is exactly what happens when such a list is not: the association\r\n * walk knew about activities and silently discarded every event's.\r\n *\r\n * The consequence for the id index is that an event's `dataInput` /\r\n * `dataOutput` and its associations are resolvable by id. That matters\r\n * beyond tidiness: an event association's `targetRef` NAMES its sibling\r\n * `dataInput`, so without this the document would contain a reference\r\n * that nothing could resolve.\r\n */\r\nfunction appendDataIoChildren(elem: BpmnElement, out: BpmnElement[]): void {\r\n const owner = elem as BpmnElement & EventDataIo & { ioSpecification?: BpmnElement };\r\n for (const slot of [\r\n owner.dataInputs,\r\n owner.dataOutputs,\r\n owner.dataInputAssociations,\r\n owner.dataOutputAssociations,\r\n owner.inputSets,\r\n owner.outputSets,\r\n ]) {\r\n if (slot !== undefined) out.push(...slot);\r\n }\r\n // An ACTIVITY keeps its declarations one level further down, inside\r\n // ``. Descending into the container is what puts\r\n // those `dataInput` / `dataOutput` ids in the index; without it an\r\n // activity association's `sourceRef` resolves to nothing and the\r\n // validator reports eight false alarms on the ARIS fixture alone.\r\n if (owner.ioSpecification !== undefined) out.push(owner.ioSpecification);\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Internal: parser context + types */\r\n/* ------------------------------------------------------------------ */\r\n\r\ninterface ParserContext {\r\n namespaces: NamespaceBinding[];\r\n warnings: string[];\r\n}\r\n\r\nfunction createContext(namespaces: NamespaceBinding[]): ParserContext {\r\n return { namespaces, warnings: [] };\r\n}\r\n\r\n/**\r\n * Shape of a node returned by fast-xml-parser with `preserveOrder: true`.\r\n * Each entry is an object with exactly one tag-name key and an optional\r\n * `:@` attribute bag. We normalise this immediately into `Element`.\r\n */\r\ninterface PreservedNode {\r\n [tagOrColon: string]: unknown;\r\n}\r\n\r\ninterface Element {\r\n /** Raw qualified name as written in the source (e.g., 'bpmn:task'). */\r\n rawName: string;\r\n /** Unprocessed attributes (key = qualified name, value = string). */\r\n attrs: Record;\r\n /** Child elements. Does not include text nodes; those go into `text`. */\r\n children: Element[];\r\n /** Concatenated text content directly under this element. */\r\n text?: string;\r\n}\r\n\r\n/**\r\n * Convert a fast-xml-parser preserved-order array into our `Element[]`.\r\n * Skips text nodes (`#text`) by promoting their value into the parent\r\n * via the caller; comments and PIs are dropped here (handled separately\r\n * for the document level).\r\n */\r\nfunction normalizeChildren(arr: PreservedNode[] | undefined): {\r\n children: Element[];\r\n text?: string;\r\n} {\r\n if (arr === undefined) return { children: [] };\r\n const children: Element[] = [];\r\n let text = '';\r\n for (const node of arr) {\r\n const key = singleKey(node);\r\n if (key === undefined) continue;\r\n if (key === '#text') {\r\n const v = node[key];\r\n if (typeof v === 'string') text += v;\r\n else if (typeof v === 'number') text += String(v);\r\n continue;\r\n }\r\n if (key === '#comment' || key.startsWith('?')) {\r\n // Drop comments / PIs inside elements (top-level handled elsewhere).\r\n continue;\r\n }\r\n const innerArr = node[key];\r\n const attrsRaw = (node[':@'] as Record | undefined) ?? {};\r\n const attrs: Record = {};\r\n for (const k of Object.keys(attrsRaw)) {\r\n const v = attrsRaw[k];\r\n if (typeof v === 'string') attrs[k] = v;\r\n else if (typeof v === 'number' || typeof v === 'boolean') attrs[k] = String(v);\r\n // null/undefined and complex values are skipped (FXP shouldn't emit these here).\r\n }\r\n const sub = normalizeChildren(Array.isArray(innerArr) ? (innerArr as PreservedNode[]) : []);\r\n const elem: Element = {\r\n rawName: key,\r\n attrs,\r\n children: sub.children,\r\n };\r\n if (sub.text !== undefined && sub.text.length > 0) elem.text = sub.text;\r\n children.push(elem);\r\n }\r\n const result: { children: Element[]; text?: string } = { children };\r\n if (text.length > 0) result.text = text;\r\n return result;\r\n}\r\n\r\nfunction singleKey(node: PreservedNode | undefined): string | undefined {\r\n if (node === undefined || node === null) return undefined;\r\n for (const k of Object.keys(node)) {\r\n if (k === ':@') continue;\r\n return k;\r\n }\r\n return undefined;\r\n}\r\n\r\nfunction extractText(arr: PreservedNode[]): string | undefined {\r\n let text = '';\r\n for (const node of arr) {\r\n const key = singleKey(node);\r\n if (key === '#text') {\r\n const v = node[key];\r\n if (typeof v === 'string') text += v;\r\n else if (typeof v === 'number') text += String(v);\r\n }\r\n }\r\n return text.length > 0 ? text : undefined;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Locate */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Where a document runs out mid-tag, and which tag it ran out inside. */\r\ninterface TruncationPoint {\r\n /** The raw tag text after '<', empty when the document ends on the '<'. */\r\n name: string;\r\n line: number;\r\n column: number;\r\n}\r\n\r\n/**\r\n * 1-based line and column of a character offset.\r\n *\r\n * All three line ends XML 1.0 defines count, not just LF: section 2.11\r\n * normalizes CRLF and a lone CR to a line end before the document is ever\r\n * seen as characters. Counting LF alone reported a CR-only document as one\r\n * long line - a location that is specific, formatted like a location, and\r\n * wrong, which is the exact failure this diagnostic was written to replace.\r\n * CRLF is consumed as ONE line end by letting the LF do the counting.\r\n */\r\nfunction positionOf(text: string, offset: number): { line: number; column: number } {\r\n let line = 1;\r\n let lineStart = 0;\r\n for (let i = 0; i < offset; i += 1) {\r\n const char = text[i];\r\n if (char === '\\r' && text[i + 1] === '\\n') continue;\r\n if (char === '\\r' || char === '\\n') {\r\n line += 1;\r\n lineStart = i + 1;\r\n }\r\n }\r\n return { line, column: offset - lineStart + 1 };\r\n}\r\n\r\n/** Index just past a `` declaration, allowing one internal subset. */\r\nfunction endOfDeclaration(text: string, start: number): number | undefined {\r\n const subset = text.indexOf('[', start);\r\n const close = text.indexOf('>', start);\r\n if (close === -1) return undefined;\r\n if (subset === -1 || subset > close) return close + 1;\r\n const subsetEnd = text.indexOf(']', subset);\r\n if (subsetEnd === -1) return undefined;\r\n const after = text.indexOf('>', subsetEnd);\r\n return after === -1 ? undefined : after + 1;\r\n}\r\n\r\ntype TagScan =\r\n | { kind: 'closed'; next: number }\r\n | { kind: 'ranOut'; name: string }\r\n | { kind: 'openQuote' };\r\n\r\n/** Scans one tag beginning at the '<' in `start`. */\r\nfunction scanTag(text: string, start: number): TagScan {\r\n let index = start + 1;\r\n while (index < text.length && !/[\\s/>]/.test(text[index] ?? '')) index += 1;\r\n const name = text.slice(start + 1, index);\r\n while (index < text.length) {\r\n const char = text[index];\r\n if (char === '\"' || char === \"'\") {\r\n const close = text.indexOf(char, index + 1);\r\n // An attribute value that never closes is the OTHER #8272 fault, and\r\n // the validator words it better (\"Attributes for 'x' have open\r\n // quote\") than anything that could be said from here.\r\n if (close === -1) return { kind: 'openQuote' };\r\n index = close + 1;\r\n continue;\r\n }\r\n if (char === '>') return { kind: 'closed', next: index + 1 };\r\n index += 1;\r\n }\r\n return { kind: 'ranOut', name };\r\n}\r\n\r\n/**\r\n * Finds the tag a document ends INSIDE, if it ends inside one.\r\n *\r\n * This exists because the validator's answer for exactly that document is\r\n * not an answer. Executed against fast-xml-parser 4.5.6, a file truncated\r\n * after `', index + 4);\r\n if (end === -1) return undefined;\r\n index = end + 3;\r\n continue;\r\n }\r\n if (text.startsWith('', index + 9);\r\n if (end === -1) return undefined;\r\n index = end + 3;\r\n continue;\r\n }\r\n if (text.startsWith('', index + 2);\r\n if (end === -1) return undefined;\r\n index = end + 2;\r\n continue;\r\n }\r\n if (text.startsWith(' 0) {\r\n return BpmnEditorError.invalidXml(\r\n `The diagram could not be read: ${cause.message.replace(/\\.$/, '')}.`,\r\n cause,\r\n );\r\n }\r\n\r\n const truncated = findTruncatedTag(xmlText);\r\n if (truncated !== undefined) {\r\n const where = `line ${String(truncated.line)}, column ${String(truncated.column)}`;\r\n const what =\r\n truncated.name.length === 0\r\n ? `the document ends immediately after a \\`<\\` at ${where}, with no tag name after it`\r\n : `the document ends part-way through the tag \\`<${truncated.name}\\`, ` +\r\n `which opens at ${where} and is never closed`;\r\n return BpmnEditorError.invalidXml(\r\n `The diagram is not well-formed XML: ${what}. The stored XML is truncated - ` +\r\n `it is missing everything from that point on.`,\r\n cause,\r\n );\r\n }\r\n\r\n const validation = XMLValidator.validate(xmlText, { allowBooleanAttributes: true });\r\n if (validation !== true) {\r\n const { msg, line, col } = validation.err;\r\n return BpmnEditorError.invalidXml(\r\n `The diagram is not well-formed XML: ${msg} (line ${String(line)}, column ${String(col)}).`,\r\n cause,\r\n );\r\n }\r\n\r\n return BpmnEditorError.invalidXml(\r\n 'The diagram could not be read: the XML reader failed part-way through ' +\r\n 'the stored document and no specific fault could be identified. The ' +\r\n 'original error is attached as detail.',\r\n cause,\r\n );\r\n}\r\n\r\n/**\r\n * Returns the raw name of the first real element at the top level, skipping\r\n * text, comments and processing instructions. Used only to make the\r\n * \"not a BPMN file\" diagnostic name what was actually found (Bug #8272).\r\n */\r\nfunction firstElementName(parsed: PreservedNode[]): string | undefined {\r\n for (const node of parsed) {\r\n const key = singleKey(node);\r\n if (key === undefined) continue;\r\n if (key === '#text' || key === '#comment' || key.startsWith('?')) continue;\r\n return key;\r\n }\r\n return undefined;\r\n}\r\n\r\nfunction findDefinitionsNode(parsed: PreservedNode[]): Element | undefined {\r\n for (const node of parsed) {\r\n const key = singleKey(node);\r\n if (key === undefined) continue;\r\n if (key === '#text' || key === '#comment' || key.startsWith('?')) continue;\r\n const localPart = localName(key);\r\n if (localPart !== 'definitions') {\r\n // Not a BPMN file (e.g., HTML, SVG, plain XML).\r\n return undefined;\r\n }\r\n const innerArr = node[key];\r\n const attrsRaw = (node[':@'] as Record | undefined) ?? {};\r\n const attrs: Record = {};\r\n for (const k of Object.keys(attrsRaw)) {\r\n const v = attrsRaw[k];\r\n if (typeof v === 'string') attrs[k] = v;\r\n else if (typeof v === 'number' || typeof v === 'boolean') attrs[k] = String(v);\r\n }\r\n const sub = normalizeChildren(Array.isArray(innerArr) ? (innerArr as PreservedNode[]) : []);\r\n const result: Element = { rawName: key, attrs, children: sub.children };\r\n if (sub.text !== undefined && sub.text.length > 0) result.text = sub.text;\r\n return result;\r\n }\r\n return undefined;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Namespace handling */\r\n/* ------------------------------------------------------------------ */\r\n\r\nfunction extractNamespaceBindings(root: Element): NamespaceBinding[] {\r\n const bindings: NamespaceBinding[] = [];\r\n for (const key of Object.keys(root.attrs)) {\r\n const value = root.attrs[key];\r\n if (value === undefined) continue;\r\n if (key === 'xmlns') {\r\n bindings.push({ prefix: '', uri: value });\r\n } else if (key.startsWith('xmlns:')) {\r\n const prefix = key.slice('xmlns:'.length);\r\n bindings.push({ prefix, uri: value });\r\n }\r\n }\r\n return bindings;\r\n}\r\n\r\nfunction resolveTagNamespace(qname: string, namespaces: NamespaceBinding[]): string | undefined {\r\n const idx = qname.indexOf(':');\r\n if (idx === -1) {\r\n const def = namespaces.find((b) => b.prefix === '');\r\n return def?.uri;\r\n }\r\n const prefix = qname.slice(0, idx);\r\n const binding = namespaces.find((b) => b.prefix === prefix);\r\n return binding?.uri;\r\n}\r\n\r\nfunction localName(qname: string): string {\r\n const idx = qname.indexOf(':');\r\n return idx === -1 ? qname : qname.slice(idx + 1);\r\n}\r\n\r\nfunction isModelNamespace(uri: string | undefined): boolean {\r\n if (uri === undefined) return false;\r\n return uri === BPMN_NS.MODEL || uri === 'http://www.omg.org/spec/BPMN/20100524/Semantic';\r\n}\r\n\r\nfunction isDcNamespace(uri: string | undefined): boolean {\r\n return uri === BPMN_NS.DC;\r\n}\r\n\r\nfunction isDiNamespace(uri: string | undefined): boolean {\r\n return uri === BPMN_NS.DI;\r\n}\r\n\r\nfunction isBpmnDiNamespace(uri: string | undefined): boolean {\r\n return uri === BPMN_NS.BPMNDI;\r\n}\r\n\r\n/**\r\n * Harvests any `xmlns*` bindings declared locally on `elem` and returns a\r\n * child-scope namespace list with those bindings taking precedence over\r\n * the parent-scope ones. When an element has no local rebindings the\r\n * parent list is returned unchanged (no allocation).\r\n *\r\n * Per research-notes/phase-1.md §1: BPMN exporters often rebind the\r\n * default namespace on `` (so its BPMNPlane/BPMNShape/etc.\r\n * children are implicitly in the DI namespace). The parent-only namespace\r\n * resolution originally shipped would miss these, drop the whole DI\r\n * subtree, and later crash `indexDeep` on an undefined plane.\r\n */\r\nfunction childScopeNamespaces(\r\n elem: Element,\r\n parent: NamespaceBinding[],\r\n): NamespaceBinding[] {\r\n let locals: NamespaceBinding[] | undefined;\r\n for (const key of Object.keys(elem.attrs)) {\r\n const value = elem.attrs[key];\r\n if (value === undefined) continue;\r\n if (key === 'xmlns') {\r\n if (locals === undefined) locals = [];\r\n locals.push({ prefix: '', uri: value });\r\n } else if (key.startsWith('xmlns:')) {\r\n if (locals === undefined) locals = [];\r\n locals.push({ prefix: key.slice('xmlns:'.length), uri: value });\r\n }\r\n }\r\n if (locals === undefined) return parent;\r\n // Child-local bindings shadow parent bindings sharing the same prefix.\r\n const shadowedPrefixes = new Set(locals.map((b) => b.prefix));\r\n const merged: NamespaceBinding[] = [];\r\n for (const local of locals) merged.push(local);\r\n for (const p of parent) {\r\n if (!shadowedPrefixes.has(p.prefix)) merged.push(p);\r\n }\r\n return merged;\r\n}\r\n\r\n/**\r\n * Runs `fn` with `ctx.namespaces` augmented by any local xmlns rebindings\r\n * on `elem`. Restores the previous list on exit. Used by any call site\r\n * that walks the children of an element whose subtree may rebind xmlns.\r\n */\r\nfunction withLocalNamespaces(ctx: ParserContext, elem: Element, fn: () => T): T {\r\n const effective = childScopeNamespaces(elem, ctx.namespaces);\r\n if (effective === ctx.namespaces) return fn();\r\n const saved = ctx.namespaces;\r\n ctx.namespaces = effective;\r\n try {\r\n return fn();\r\n } finally {\r\n ctx.namespaces = saved;\r\n }\r\n}\r\n\r\n/**\r\n * Resolves a child element's tag namespace using a scope that includes\r\n * the child's own `xmlns` / `xmlns:*` declarations (not just the\r\n * parent's). Required because authoring tools such as Bizagi rebind the\r\n * default namespace directly on DI elements (e.g. ``\r\n * nested inside a `` that itself inherits `xmlns=\"…/DI\"`).\r\n * Without this scoping, the Bounds child resolves to the parent's default\r\n * (DI), fails the `isDcNamespace` check, and the shape loses its bounds.\r\n */\r\nfunction resolveChildTagNamespace(child: Element, parent: NamespaceBinding[]): string | undefined {\r\n const effective = childScopeNamespaces(child, parent);\r\n return resolveTagNamespace(child.rawName, effective);\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Attribute splitting (known vs foreign) */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/**\r\n * Walks every attribute on an element and routes it to either the typed\r\n * known-attribute callback or the foreign-attribute sink. Skips xmlns\r\n * declarations entirely.\r\n */\r\nfunction splitAttributes(\r\n attrs: Record,\r\n ctx: ParserContext,\r\n namespaces: NamespaceBinding[],\r\n onKnown: (localName: string, value: string) => void,\r\n onForeign: (foreign: ForeignAttribute) => void,\r\n): void {\r\n for (const key of Object.keys(attrs)) {\r\n const value = attrs[key];\r\n if (value === undefined) continue;\r\n if (key === 'xmlns' || key.startsWith('xmlns:')) continue;\r\n\r\n const idx = key.indexOf(':');\r\n if (idx === -1) {\r\n // Unprefixed attribute. Per the XML namespace spec these belong to\r\n // no namespace (NOT the element's default namespace).\r\n onKnown(key, value);\r\n continue;\r\n }\r\n const prefix = key.slice(0, idx);\r\n const local = key.slice(idx + 1);\r\n const binding = namespaces.find((b) => b.prefix === prefix);\r\n const uri = binding?.uri;\r\n\r\n if (prefix === 'xsi' || uri === BPMN_NS.XSI) {\r\n // Preserve xsi:* (e.g., schemaLocation) as a foreign attribute.\r\n const fa: ForeignAttribute = {\r\n namespaceUri: BPMN_NS.XSI,\r\n localName: local,\r\n value,\r\n };\r\n if (prefix.length > 0) fa.prefix = prefix;\r\n onForeign(fa);\r\n continue;\r\n }\r\n\r\n if (uri !== undefined && KNOWN_BPMN_NAMESPACE_URIS.has(uri)) {\r\n // Prefixed with a BPMN-owned URI — treat the local name as known.\r\n onKnown(local, value);\r\n continue;\r\n }\r\n\r\n // Foreign-namespace attribute (vendor metadata).\r\n if (uri === undefined || uri.length === 0) {\r\n ctx.warnings.push(`Unknown attribute prefix '${prefix}' on '${key}' — preserving as foreign.`);\r\n }\r\n const fa: ForeignAttribute = {\r\n namespaceUri: uri ?? '',\r\n localName: local,\r\n value,\r\n };\r\n if (prefix.length > 0) fa.prefix = prefix;\r\n onForeign(fa);\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Foreign-node conversion */\r\n/* ------------------------------------------------------------------ */\r\n\r\n/** Convert any element into a verbatim `ForeignNode` tree. */\r\nfunction nodeToForeign(elem: Element, namespaces: NamespaceBinding[]): ForeignNode | undefined {\r\n // Extend the inherited namespace list with any xmlns declarations\r\n // carried on the element itself BEFORE resolving its own name. This\r\n // matches XML namespace scoping: an element declares a prefix on\r\n // itself and then uses that prefix in its own tag name. Without this\r\n // step, foreign nodes whose vendor xmlns is declared locally (common\r\n // for `` fragments inside\r\n // ``) land with namespaceUri=\"\" and round-trip\r\n // into the parent's default namespace instead of the vendor's.\r\n const effective = childScopeNamespaces(elem, namespaces);\r\n const ns = resolveTagNamespace(elem.rawName, effective) ?? '';\r\n const idx = elem.rawName.indexOf(':');\r\n const prefix = idx === -1 ? '' : elem.rawName.slice(0, idx);\r\n const local = localName(elem.rawName);\r\n const attributes: ForeignAttribute[] = [];\r\n for (const key of Object.keys(elem.attrs)) {\r\n const value = elem.attrs[key];\r\n if (value === undefined) continue;\r\n if (key === 'xmlns' || key.startsWith('xmlns:')) continue;\r\n const aIdx = key.indexOf(':');\r\n const aPrefix = aIdx === -1 ? '' : key.slice(0, aIdx);\r\n const aLocal = aIdx === -1 ? key : key.slice(aIdx + 1);\r\n const aBinding = aPrefix.length > 0\r\n ? effective.find((b) => b.prefix === aPrefix)\r\n : undefined;\r\n const fa: ForeignAttribute = {\r\n namespaceUri: aBinding?.uri ?? '',\r\n localName: aLocal,\r\n value,\r\n };\r\n if (aPrefix.length > 0) fa.prefix = aPrefix;\r\n attributes.push(fa);\r\n }\r\n const children: ForeignNode[] = [];\r\n for (const c of elem.children) {\r\n // Pass the effective (extended) namespace list so children inherit\r\n // the locally-declared prefixes we just picked up.\r\n const childForeign = nodeToForeign(c, effective);\r\n if (childForeign !== undefined) children.push(childForeign);\r\n }\r\n const result: ForeignNode = {\r\n namespaceUri: ns,\r\n localName: local,\r\n attributes,\r\n children,\r\n };\r\n if (prefix.length > 0) result.prefix = prefix;\r\n if (elem.text !== undefined && elem.text.length > 0) result.text = elem.text;\r\n return result;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Common helpers shared across parsers */\r\n/* ------------------------------------------------------------------ */\r\n\r\nfunction popId(attrs: Record): string {\r\n const id = attrs['id'];\r\n return id === undefined ? '' : id;\r\n}\r\n\r\nfunction popName(attrs: Record): string | undefined {\r\n return attrs['name'];\r\n}\r\n\r\nfunction parseBoolean(value: string | undefined): boolean | undefined {\r\n if (value === undefined) return undefined;\r\n if (value === 'true' || value === '1') return true;\r\n if (value === 'false' || value === '0') return false;\r\n return undefined;\r\n}\r\n\r\nfunction parseFloatOrZero(value: string | undefined): number {\r\n if (value === undefined) return 0;\r\n const n = Number.parseFloat(value);\r\n return Number.isFinite(n) ? n : 0;\r\n}\r\n\r\nfunction indexElement(document: BpmnDocument, elem: BpmnElement): void {\r\n if (elem.id.length === 0) return;\r\n if (document.byId.has(elem.id)) {\r\n // Duplicate id — keep first occurrence per research-notes §3.\r\n return;\r\n }\r\n document.byId.set(elem.id, elem);\r\n}\r\n\r\n/**\r\n * Common header parse: pulls out id/name/documentation, partitions\r\n * attributes into known vs foreign, captures `` and\r\n * `` children. Returns the residual children that\r\n * type-specific parsers walk.\r\n */\r\nfunction parseElementHeader(\r\n elem: Element,\r\n ctx: ParserContext,\r\n knownAttrs: Record,\r\n): {\r\n id: string;\r\n name: string | undefined;\r\n documentation: string[];\r\n extensionElements: ForeignNode[];\r\n foreignAttributes: ForeignAttribute[];\r\n bodyOrder: { kind: string; id?: string }[];\r\n remaining: Element[];\r\n} {\r\n const foreignAttributes: ForeignAttribute[] = [];\r\n splitAttributes(\r\n elem.attrs,\r\n ctx,\r\n ctx.namespaces,\r\n (local, value) => {\r\n knownAttrs[local] = value;\r\n },\r\n (foreign) => {\r\n foreignAttributes.push(foreign);\r\n },\r\n );\r\n\r\n const id = popId(knownAttrs);\r\n const name = popName(knownAttrs);\r\n const documentation: string[] = [];\r\n const extensionElements: ForeignNode[] = [];\r\n const remaining: Element[] = [];\r\n const bodyOrder: { kind: string; id?: string }[] = [];\r\n\r\n for (const child of elem.children) {\r\n const ns = resolveTagNamespace(child.rawName, ctx.namespaces);\r\n const local = localName(child.rawName);\r\n if (isModelNamespace(ns)) {\r\n if (local === 'documentation') {\r\n documentation.push(child.text ?? '');\r\n bodyOrder.push({ kind: 'documentation' });\r\n continue;\r\n }\r\n if (local === 'extensionElements') {\r\n for (const grand of child.children) {\r\n const fn = nodeToForeign(grand, ctx.namespaces);\r\n if (fn !== undefined) extensionElements.push(fn);\r\n }\r\n bodyOrder.push({ kind: 'extensionElements' });\r\n continue;\r\n }\r\n remaining.push(child);\r\n continue;\r\n }\r\n if (isBpmnDiNamespace(ns) || isDcNamespace(ns) || isDiNamespace(ns)) {\r\n // BPMN-owned but non-MODEL element appearing as a child of a MODEL\r\n // element. Defer to the type-specific parser via remaining[].\r\n remaining.push(child);\r\n continue;\r\n }\r\n // Foreign-namespace child of a MODEL element (vendor inline).\r\n const fn = nodeToForeign(child, ctx.namespaces);\r\n if (fn !== undefined) {\r\n extensionElements.push(fn);\r\n bodyOrder.push({ kind: fn.localName });\r\n }\r\n }\r\n\r\n return {\r\n id,\r\n name,\r\n documentation,\r\n extensionElements,\r\n foreignAttributes,\r\n bodyOrder,\r\n remaining,\r\n };\r\n}\r\n\r\nfunction applyHeader(target: T, header: {\r\n id: string;\r\n name: string | undefined;\r\n documentation: string[];\r\n extensionElements: ForeignNode[];\r\n foreignAttributes: ForeignAttribute[];\r\n bodyOrder: { kind: string; id?: string }[];\r\n}): void {\r\n target.id = header.id;\r\n if (header.name !== undefined) target.name = header.name;\r\n if (header.documentation.length > 0) target.documentation = header.documentation;\r\n target.extensionElements = header.extensionElements;\r\n target.foreignAttributes = header.foreignAttributes;\r\n if (header.bodyOrder.length > 0) target.bodyOrder = header.bodyOrder;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Process */\r\n/* ------------------------------------------------------------------ */\r\n\r\nfunction parseProcess(elem: Element, ctx: ParserContext): Process {\r\n const knownAttrs: Record = {};\r\n const header = parseElementHeader(elem, ctx, knownAttrs);\r\n const proc: Process = {\r\n type: 'process',\r\n id: header.id,\r\n extensionElements: header.extensionElements,\r\n foreignAttributes: header.foreignAttributes,\r\n };\r\n applyHeader(proc, header);\r\n\r\n const isExecutable = parseBoolean(knownAttrs['isExecutable']);\r\n if (isExecutable !== undefined) proc.isExecutable = isExecutable;\r\n const isClosed = parseBoolean(knownAttrs['isClosed']);\r\n if (isClosed !== undefined) proc.isClosed = isClosed;\r\n const processType = knownAttrs['processType'];\r\n if (processType === 'None' || processType === 'Public' || processType === 'Private') {\r\n proc.processType = processType as ProcessType;\r\n }\r\n const defColl = knownAttrs['definitionalCollaborationRef'];\r\n if (defColl !== undefined) proc.definitionalCollaborationRef = defColl;\r\n\r\n const flowElements: BpmnElement[] = [];\r\n const artifacts: BpmnElement[] = [];\r\n const laneSets: BpmnElement[] = [];\r\n\r\n for (const child of header.remaining) {\r\n const ns = resolveTagNamespace(child.rawName, ctx.namespaces);\r\n if (!isModelNamespace(ns)) {\r\n const fn = nodeToForeign(child, ctx.namespaces);\r\n if (fn !== undefined) proc.extensionElements.push(fn);\r\n continue;\r\n }\r\n const local = localName(child.rawName);\r\n const parsed = parseProcessChild(local, child, ctx);\r\n if (parsed === undefined) continue;\r\n if (isLaneSetType(parsed.type)) {\r\n laneSets.push(parsed);\r\n } else if (isArtifactType(parsed.type)) {\r\n artifacts.push(parsed);\r\n } else {\r\n flowElements.push(parsed);\r\n }\r\n if (proc.bodyOrder !== undefined) {\r\n proc.bodyOrder.push({ kind: parsed.type, id: parsed.id });\r\n }\r\n }\r\n\r\n if (flowElements.length > 0) proc.flowElements = flowElements;\r\n if (artifacts.length > 0) proc.artifacts = artifacts;\r\n if (laneSets.length > 0) proc.laneSets = laneSets;\r\n return proc;\r\n}\r\n\r\nfunction isLaneSetType(t: string): boolean {\r\n return t === 'laneSet';\r\n}\r\n\r\n\r\n/**\r\n * Dispatch a single child of `` / `` / event subprocess.\r\n * Returns `undefined` when the child has no typed shape (rare; vendor\r\n * extensions are handled by the caller).\r\n */\r\nfunction parseProcessChild(local: string, elem: Element, ctx: ParserContext): BpmnElement | undefined {\r\n switch (local) {\r\n case 'task':\r\n case 'userTask':\r\n case 'serviceTask':\r\n case 'sendTask':\r\n case 'receiveTask':\r\n case 'manualTask':\r\n case 'businessRuleTask':\r\n case 'scriptTask':\r\n return parseTask(elem, ctx, local);\r\n case 'callActivity':\r\n return parseCallActivity(elem, ctx);\r\n case 'subProcess':\r\n return parseSubProcess(elem, ctx);\r\n case 'transaction':\r\n return parseTransaction(elem, ctx);\r\n case 'adHocSubProcess':\r\n return parseAdHocSubProcess(elem, ctx);\r\n case 'startEvent':\r\n return parseStartEvent(elem, ctx);\r\n case 'endEvent':\r\n return parseEndEvent(elem, ctx);\r\n case 'intermediateThrowEvent':\r\n return parseIntermediateThrowEvent(elem, ctx);\r\n case 'intermediateCatchEvent':\r\n return parseIntermediateCatchEvent(elem, ctx);\r\n case 'boundaryEvent':\r\n return parseBoundaryEvent(elem, ctx);\r\n case 'exclusiveGateway':\r\n case 'parallelGateway':\r\n case 'inclusiveGateway':\r\n case 'eventBasedGateway':\r\n case 'complexGateway':\r\n return parseGateway(elem, ctx, local);\r\n case 'sequenceFlow':\r\n return parseSequenceFlow(elem, ctx);\r\n case 'dataObject':\r\n return parseDataObject(elem, ctx);\r\n case 'dataObjectReference':\r\n return parseDataObjectReference(elem, ctx);\r\n case 'dataStoreReference':\r\n return parseDataStoreReference(elem, ctx);\r\n case 'textAnnotation':\r\n return parseTextAnnotation(elem, ctx);\r\n case 'group':\r\n return parseGroup(elem, ctx);\r\n case 'association':\r\n return parseAssociation(elem, ctx);\r\n case 'laneSet':\r\n return parseLaneSet(elem, ctx);\r\n default:\r\n // Unknown BPMN-namespace child — preserve via generic container.\r\n return parseGenericContainer(elem, ctx, local);\r\n }\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Generic container fallback */\r\n/* ------------------------------------------------------------------ */\r\n\r\nfunction parseGenericContainer(elem: Element, ctx: ParserContext, local: string): BpmnElement {\r\n const knownAttrs: Record = {};\r\n const header = parseElementHeader(elem, ctx, knownAttrs);\r\n const generic: BpmnElement = {\r\n type: local,\r\n id: header.id,\r\n extensionElements: header.extensionElements,\r\n foreignAttributes: header.foreignAttributes,\r\n };\r\n applyHeader(generic, header);\r\n // Remaining children flattened into extensionElements as foreign nodes\r\n // (best-effort preservation for round-trip).\r\n for (const child of header.remaining) {\r\n const fn = nodeToForeign(child, ctx.namespaces);\r\n if (fn !== undefined) generic.extensionElements.push(fn);\r\n }\r\n return generic;\r\n}\r\n\r\n/* ------------------------------------------------------------------ */\r\n/* Task & subclasses */\r\n/* ------------------------------------------------------------------ */\r\n\r\ntype TaskLocalName =\r\n | 'task'\r\n | 'userTask'\r\n | 'serviceTask'\r\n | 'sendTask'\r\n | 'receiveTask'\r\n | 'manualTask'\r\n | 'businessRuleTask'\r\n | 'scriptTask';\r\n\r\nfunction parseTask(elem: Element, ctx: ParserContext, local: TaskLocalName): BpmnElement {\r\n const knownAttrs: Record = {};\r\n const header = parseElementHeader(elem, ctx, knownAttrs);\r\n\r\n // Build the type-specific shape and assign type-specific attrs.\r\n let result:\r\n | Task\r\n | UserTask\r\n | ServiceTask\r\n | SendTask\r\n | ReceiveTask\r\n | ManualTask\r\n | BusinessRuleTask\r\n | ScriptTask;\r\n\r\n switch (local) {\r\n case 'task':\r\n result = { type: 'task', id: header.id, extensionElements: [], foreignAttributes: [] };\r\n break;\r\n case 'userTask': {\r\n const r: UserTask = { type: 'userTask', id: header.id, extensionElements: [], foreignAttributes: [] };\r\n if (knownAttrs['implementation'] !== undefined) r.implementation = knownAttrs['implementation'];\r\n result = r;\r\n break;\r\n }\r\n case 'serviceTask': {\r\n const r: ServiceTask = {\r\n type: 'serviceTask',\r\n id: header.id,\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n };\r\n if (knownAttrs['implementation'] !== undefined) r.implementation = knownAttrs['implementation'];\r\n if (knownAttrs['operationRef'] !== undefined) r.operationRef = knownAttrs['operationRef'];\r\n result = r;\r\n break;\r\n }\r\n case 'sendTask': {\r\n const r: SendTask = {\r\n type: 'sendTask',\r\n id: header.id,\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n };\r\n if (knownAttrs['implementation'] !== undefined) r.implementation = knownAttrs['implementation'];\r\n if (knownAttrs['messageRef'] !== undefined) r.messageRef = knownAttrs['messageRef'];\r\n if (knownAttrs['operationRef'] !== undefined) r.operationRef = knownAttrs['operationRef'];\r\n result = r;\r\n break;\r\n }\r\n case 'receiveTask': {\r\n const r: ReceiveTask = {\r\n type: 'receiveTask',\r\n id: header.id,\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n };\r\n if (knownAttrs['implementation'] !== undefined) r.implementation = knownAttrs['implementation'];\r\n const inst = parseBoolean(knownAttrs['instantiate']);\r\n if (inst !== undefined) r.instantiate = inst;\r\n if (knownAttrs['messageRef'] !== undefined) r.messageRef = knownAttrs['messageRef'];\r\n if (knownAttrs['operationRef'] !== undefined) r.operationRef = knownAttrs['operationRef'];\r\n result = r;\r\n break;\r\n }\r\n case 'manualTask':\r\n result = { type: 'manualTask', id: header.id, extensionElements: [], foreignAttributes: [] };\r\n break;\r\n case 'businessRuleTask': {\r\n const r: BusinessRuleTask = {\r\n type: 'businessRuleTask',\r\n id: header.id,\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n };\r\n if (knownAttrs['implementation'] !== undefined) r.implementation = knownAttrs['implementation'];\r\n result = r;\r\n break;\r\n }\r\n case 'scriptTask': {\r\n const r: ScriptTask = {\r\n type: 'scriptTask',\r\n id: header.id,\r\n extensionElements: [],\r\n foreignAttributes: [],\r\n };\r\n if (knownAttrs['scriptFormat'] !== undefined) r.scriptFormat = knownAttrs['scriptFormat'];\r\n result = r;\r\n break;\r\n }\r\n default:\r\n // Exhaustiveness — unreachable given the parameter type.\r\n result = { type: 'task', id: header.id, extensionElements: [], foreignAttributes: [] };\r\n }\r\n\r\n applyHeader(result, header);\r\n applyActivityCommon(result, knownAttrs, header.remaining, ctx);\r\n\r\n if (local === 'scriptTask') {\r\n // Inline script body lives as a `