hoi4-agent-tools 0.2.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -118
- package/README.md +38 -124
- package/SECURITY.md +5 -7
- package/dist/bin/http.js +0 -1
- package/dist/bin/setup.js +141 -92
- package/dist/bin/stdio.js +0 -1
- package/dist/hoi4_agent_tools/core/artifacts.js +0 -1
- package/dist/hoi4_agent_tools/core/canonical.js +0 -1
- package/dist/hoi4_agent_tools/core/configuration.js +28 -28
- package/dist/hoi4_agent_tools/core/diagnostic-collector.js +0 -1
- package/dist/hoi4_agent_tools/core/diagnostics.js +0 -1
- package/dist/hoi4_agent_tools/core/diff.js +0 -1
- package/dist/hoi4_agent_tools/core/engine.js +6 -21
- package/dist/hoi4_agent_tools/core/image-diff.js +0 -1
- package/dist/hoi4_agent_tools/core/index.js +0 -1
- package/dist/hoi4_agent_tools/core/render-budget.js +0 -1
- package/dist/hoi4_agent_tools/core/result.js +0 -1
- package/dist/hoi4_agent_tools/core/scanner.js +0 -1
- package/dist/hoi4_agent_tools/core/server-state.js +0 -1
- package/dist/hoi4_agent_tools/core/source/encoding.js +0 -1
- package/dist/hoi4_agent_tools/core/source/index.js +0 -1
- package/dist/hoi4_agent_tools/core/source/lexer.js +0 -1
- package/dist/hoi4_agent_tools/core/source/limits.js +0 -1
- package/dist/hoi4_agent_tools/core/source/localisation.js +0 -1
- package/dist/hoi4_agent_tools/core/source/parser.js +0 -1
- package/dist/hoi4_agent_tools/core/source/rewrite.js +0 -1
- package/dist/hoi4_agent_tools/core/svg-text.js +0 -1
- package/dist/hoi4_agent_tools/core/transaction-limits.js +0 -1
- package/dist/hoi4_agent_tools/core/transactions.js +2 -30
- package/dist/hoi4_agent_tools/core/workspace.js +112 -396
- package/dist/hoi4_agent_tools/focus/compact.js +432 -0
- package/dist/hoi4_agent_tools/focus/compiler.js +1 -8
- package/dist/hoi4_agent_tools/focus/continuous-render.js +0 -1
- package/dist/hoi4_agent_tools/focus/drift.js +0 -1
- package/dist/hoi4_agent_tools/focus/geometry.js +203 -17
- package/dist/hoi4_agent_tools/focus/importer.js +0 -1
- package/dist/hoi4_agent_tools/focus/index.js +1 -1
- package/dist/hoi4_agent_tools/focus/layout.js +1256 -93
- package/dist/hoi4_agent_tools/focus/limits.js +1 -2
- package/dist/hoi4_agent_tools/focus/lint.js +11 -4
- package/dist/hoi4_agent_tools/focus/model.js +0 -1
- package/dist/hoi4_agent_tools/focus/native-effects.js +0 -1
- package/dist/hoi4_agent_tools/focus/planning.js +6 -2
- package/dist/hoi4_agent_tools/focus/presentation.js +0 -1
- package/dist/hoi4_agent_tools/focus/render.js +14 -10
- package/dist/hoi4_agent_tools/focus/service.js +7 -8
- package/dist/hoi4_agent_tools/focus/source-update.js +1 -2
- package/dist/hoi4_agent_tools/gui/animation-manifest.js +0 -1
- package/dist/hoi4_agent_tools/gui/assets.js +0 -1
- package/dist/hoi4_agent_tools/gui/dds.js +0 -1
- package/dist/hoi4_agent_tools/gui/helpers.js +1 -2
- package/dist/hoi4_agent_tools/gui/index.js +0 -1
- package/dist/hoi4_agent_tools/gui/layout.js +0 -1
- package/dist/hoi4_agent_tools/gui/limits.js +0 -1
- package/dist/hoi4_agent_tools/gui/renderer.js +0 -1
- package/dist/hoi4_agent_tools/gui/scenario.js +0 -1
- package/dist/hoi4_agent_tools/gui/source-graph.js +0 -1
- package/dist/hoi4_agent_tools/gui/source-patch.js +1 -2
- package/dist/hoi4_agent_tools/gui/studio.js +247 -104
- package/dist/hoi4_agent_tools/gui/tga.js +0 -1
- package/dist/hoi4_agent_tools/gui/types.js +0 -1
- package/dist/hoi4_agent_tools/gui/validators.js +0 -1
- package/dist/hoi4_agent_tools/map/bmp.js +0 -1
- package/dist/hoi4_agent_tools/map/diagnostic-limit.js +0 -1
- package/dist/hoi4_agent_tools/map/index.js +0 -1
- package/dist/hoi4_agent_tools/map/limits.js +2 -1
- package/dist/hoi4_agent_tools/map/model.js +0 -1
- package/dist/hoi4_agent_tools/map/operations.js +0 -1
- package/dist/hoi4_agent_tools/map/render.js +0 -1
- package/dist/hoi4_agent_tools/map/service.js +171 -33
- package/dist/hoi4_agent_tools/map/validation.js +0 -1
- package/dist/hoi4_agent_tools/mcp/resources/register.js +61 -187
- package/dist/hoi4_agent_tools/mcp/security/auth.js +1 -2
- package/dist/hoi4_agent_tools/mcp/server/base-tools.js +24 -750
- package/dist/hoi4_agent_tools/mcp/server/context-schemas.js +8 -0
- package/dist/hoi4_agent_tools/mcp/server/create.js +2 -12
- package/dist/hoi4_agent_tools/mcp/server/output-schemas.js +0 -71
- package/dist/hoi4_agent_tools/mcp/server/progress.js +0 -1
- package/dist/hoi4_agent_tools/mcp/server/result.js +146 -81
- package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +10 -14
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +309 -279
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +156 -254
- package/dist/hoi4_agent_tools/mcp/tools/map.js +259 -338
- package/dist/hoi4_agent_tools/mcp/tools/mods.js +54 -0
- package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.js +0 -1
- package/dist/hoi4_agent_tools/mcp/transports/event-store.js +0 -1
- package/dist/hoi4_agent_tools/mcp/transports/http.js +1 -12
- package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.js +0 -1
- package/dist/hoi4_agent_tools/runtime.js +5 -2
- package/dist/hoi4_agent_tools/schemas/common.js +1 -2
- package/dist/hoi4_agent_tools/schemas/focus.js +64 -9
- package/dist/hoi4_agent_tools/schemas/map.js +0 -1
- package/dist/hoi4_agent_tools/schemas/transaction.js +0 -1
- package/dist/hoi4_agent_tools/version.js +1 -2
- package/docs/README.md +8 -21
- package/docs/development.md +22 -0
- package/docs/focus.md +50 -0
- package/docs/gui.md +37 -0
- package/docs/http.md +53 -0
- package/docs/map.md +39 -0
- package/docs/setup.md +39 -0
- package/package.json +12 -13
- package/server.json +3 -12
- package/dist/bin/http.d.ts +0 -3
- package/dist/bin/http.d.ts.map +0 -1
- package/dist/bin/http.js.map +0 -1
- package/dist/bin/setup.d.ts +0 -3
- package/dist/bin/setup.d.ts.map +0 -1
- package/dist/bin/setup.js.map +0 -1
- package/dist/bin/stdio.d.ts +0 -3
- package/dist/bin/stdio.d.ts.map +0 -1
- package/dist/bin/stdio.js.map +0 -1
- package/dist/hoi4_agent_tools/core/artifacts.d.ts +0 -117
- package/dist/hoi4_agent_tools/core/artifacts.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/artifacts.js.map +0 -1
- package/dist/hoi4_agent_tools/core/canonical.d.ts +0 -13
- package/dist/hoi4_agent_tools/core/canonical.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/canonical.js.map +0 -1
- package/dist/hoi4_agent_tools/core/configuration.d.ts +0 -138
- package/dist/hoi4_agent_tools/core/configuration.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/configuration.js.map +0 -1
- package/dist/hoi4_agent_tools/core/diagnostic-collector.d.ts +0 -18
- package/dist/hoi4_agent_tools/core/diagnostic-collector.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/diagnostic-collector.js.map +0 -1
- package/dist/hoi4_agent_tools/core/diagnostics.d.ts +0 -26
- package/dist/hoi4_agent_tools/core/diagnostics.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/diagnostics.js.map +0 -1
- package/dist/hoi4_agent_tools/core/diff.d.ts +0 -22
- package/dist/hoi4_agent_tools/core/diff.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/diff.js.map +0 -1
- package/dist/hoi4_agent_tools/core/engine.d.ts +0 -57
- package/dist/hoi4_agent_tools/core/engine.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/engine.js.map +0 -1
- package/dist/hoi4_agent_tools/core/image-diff.d.ts +0 -14
- package/dist/hoi4_agent_tools/core/image-diff.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/image-diff.js.map +0 -1
- package/dist/hoi4_agent_tools/core/index.d.ts +0 -64
- package/dist/hoi4_agent_tools/core/index.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/index.js.map +0 -1
- package/dist/hoi4_agent_tools/core/index.public.d.ts +0 -16
- package/dist/hoi4_agent_tools/core/index.public.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/index.public.js +0 -16
- package/dist/hoi4_agent_tools/core/index.public.js.map +0 -1
- package/dist/hoi4_agent_tools/core/render-budget.d.ts +0 -51
- package/dist/hoi4_agent_tools/core/render-budget.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/render-budget.js.map +0 -1
- package/dist/hoi4_agent_tools/core/result.d.ts +0 -45
- package/dist/hoi4_agent_tools/core/result.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/result.js.map +0 -1
- package/dist/hoi4_agent_tools/core/scanner.d.ts +0 -28
- package/dist/hoi4_agent_tools/core/scanner.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/scanner.js.map +0 -1
- package/dist/hoi4_agent_tools/core/server-state.d.ts +0 -31
- package/dist/hoi4_agent_tools/core/server-state.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/server-state.js.map +0 -1
- package/dist/hoi4_agent_tools/core/source/encoding.d.ts +0 -10
- package/dist/hoi4_agent_tools/core/source/encoding.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/source/encoding.js.map +0 -1
- package/dist/hoi4_agent_tools/core/source/index.d.ts +0 -7
- package/dist/hoi4_agent_tools/core/source/index.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/source/index.js.map +0 -1
- package/dist/hoi4_agent_tools/core/source/lexer.d.ts +0 -30
- package/dist/hoi4_agent_tools/core/source/lexer.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/source/lexer.js.map +0 -1
- package/dist/hoi4_agent_tools/core/source/limits.d.ts +0 -21
- package/dist/hoi4_agent_tools/core/source/limits.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/source/limits.js.map +0 -1
- package/dist/hoi4_agent_tools/core/source/localisation.d.ts +0 -20
- package/dist/hoi4_agent_tools/core/source/localisation.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/source/localisation.js.map +0 -1
- package/dist/hoi4_agent_tools/core/source/parser.d.ts +0 -44
- package/dist/hoi4_agent_tools/core/source/parser.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/source/parser.js.map +0 -1
- package/dist/hoi4_agent_tools/core/source/rewrite.d.ts +0 -10
- package/dist/hoi4_agent_tools/core/source/rewrite.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/source/rewrite.js.map +0 -1
- package/dist/hoi4_agent_tools/core/svg-text.d.ts +0 -34
- package/dist/hoi4_agent_tools/core/svg-text.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/svg-text.js.map +0 -1
- package/dist/hoi4_agent_tools/core/transaction-limits.d.ts +0 -9
- package/dist/hoi4_agent_tools/core/transaction-limits.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/transaction-limits.js.map +0 -1
- package/dist/hoi4_agent_tools/core/transactions.d.ts +0 -164
- package/dist/hoi4_agent_tools/core/transactions.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/transactions.js.map +0 -1
- package/dist/hoi4_agent_tools/core/workspace.d.ts +0 -68
- package/dist/hoi4_agent_tools/core/workspace.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/core/workspace.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/compiler.d.ts +0 -10
- package/dist/hoi4_agent_tools/focus/compiler.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/compiler.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/continuous-render.d.ts +0 -32
- package/dist/hoi4_agent_tools/focus/continuous-render.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/continuous-render.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/drift.d.ts +0 -22
- package/dist/hoi4_agent_tools/focus/drift.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/drift.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/geometry.d.ts +0 -17
- package/dist/hoi4_agent_tools/focus/geometry.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/geometry.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/importer.d.ts +0 -9
- package/dist/hoi4_agent_tools/focus/importer.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/importer.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/index.d.ts +0 -14
- package/dist/hoi4_agent_tools/focus/index.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/index.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/layout.d.ts +0 -5
- package/dist/hoi4_agent_tools/focus/layout.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/layout.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/limits.d.ts +0 -9
- package/dist/hoi4_agent_tools/focus/limits.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/limits.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/lint.d.ts +0 -14
- package/dist/hoi4_agent_tools/focus/lint.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/lint.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/model.d.ts +0 -307
- package/dist/hoi4_agent_tools/focus/model.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/model.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/native-effects.d.ts +0 -9
- package/dist/hoi4_agent_tools/focus/native-effects.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/native-effects.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/planning.d.ts +0 -15
- package/dist/hoi4_agent_tools/focus/planning.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/planning.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/presentation.d.ts +0 -24
- package/dist/hoi4_agent_tools/focus/presentation.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/presentation.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/render.d.ts +0 -39
- package/dist/hoi4_agent_tools/focus/render.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/render.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/service.d.ts +0 -82
- package/dist/hoi4_agent_tools/focus/service.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/service.js.map +0 -1
- package/dist/hoi4_agent_tools/focus/source-update.d.ts +0 -6
- package/dist/hoi4_agent_tools/focus/source-update.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/focus/source-update.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/animation-manifest.d.ts +0 -37
- package/dist/hoi4_agent_tools/gui/animation-manifest.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/animation-manifest.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/assets.d.ts +0 -71
- package/dist/hoi4_agent_tools/gui/assets.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/assets.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/dds.d.ts +0 -17
- package/dist/hoi4_agent_tools/gui/dds.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/dds.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/helpers.d.ts +0 -101
- package/dist/hoi4_agent_tools/gui/helpers.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/helpers.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/index.d.ts +0 -15
- package/dist/hoi4_agent_tools/gui/index.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/index.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/layout.d.ts +0 -6
- package/dist/hoi4_agent_tools/gui/layout.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/layout.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/limits.d.ts +0 -25
- package/dist/hoi4_agent_tools/gui/limits.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/limits.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/renderer.d.ts +0 -18
- package/dist/hoi4_agent_tools/gui/renderer.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/renderer.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/scenario.d.ts +0 -66
- package/dist/hoi4_agent_tools/gui/scenario.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/scenario.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/source-graph.d.ts +0 -7
- package/dist/hoi4_agent_tools/gui/source-graph.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/source-graph.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/source-patch.d.ts +0 -13
- package/dist/hoi4_agent_tools/gui/source-patch.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/source-patch.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/studio.d.ts +0 -86
- package/dist/hoi4_agent_tools/gui/studio.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/studio.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/tga.d.ts +0 -16
- package/dist/hoi4_agent_tools/gui/tga.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/tga.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/types.d.ts +0 -322
- package/dist/hoi4_agent_tools/gui/types.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/types.js.map +0 -1
- package/dist/hoi4_agent_tools/gui/validators.d.ts +0 -8
- package/dist/hoi4_agent_tools/gui/validators.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/gui/validators.js.map +0 -1
- package/dist/hoi4_agent_tools/map/bmp.d.ts +0 -62
- package/dist/hoi4_agent_tools/map/bmp.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/map/bmp.js.map +0 -1
- package/dist/hoi4_agent_tools/map/diagnostic-limit.d.ts +0 -5
- package/dist/hoi4_agent_tools/map/diagnostic-limit.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/map/diagnostic-limit.js.map +0 -1
- package/dist/hoi4_agent_tools/map/index.d.ts +0 -8
- package/dist/hoi4_agent_tools/map/index.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/map/index.js.map +0 -1
- package/dist/hoi4_agent_tools/map/limits.d.ts +0 -13
- package/dist/hoi4_agent_tools/map/limits.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/map/limits.js.map +0 -1
- package/dist/hoi4_agent_tools/map/model.d.ts +0 -269
- package/dist/hoi4_agent_tools/map/model.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/map/model.js.map +0 -1
- package/dist/hoi4_agent_tools/map/operations.d.ts +0 -401
- package/dist/hoi4_agent_tools/map/operations.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/map/operations.js.map +0 -1
- package/dist/hoi4_agent_tools/map/render.d.ts +0 -90
- package/dist/hoi4_agent_tools/map/render.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/map/render.js.map +0 -1
- package/dist/hoi4_agent_tools/map/service.d.ts +0 -81
- package/dist/hoi4_agent_tools/map/service.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/map/service.js.map +0 -1
- package/dist/hoi4_agent_tools/map/validation.d.ts +0 -23
- package/dist/hoi4_agent_tools/map/validation.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/map/validation.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/prompts/register.d.ts +0 -4
- package/dist/hoi4_agent_tools/mcp/prompts/register.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/prompts/register.js +0 -72
- package/dist/hoi4_agent_tools/mcp/prompts/register.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/registry/metadata.d.ts +0 -9
- package/dist/hoi4_agent_tools/mcp/registry/metadata.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/registry/metadata.js +0 -10
- package/dist/hoi4_agent_tools/mcp/registry/metadata.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/resources/register.d.ts +0 -5
- package/dist/hoi4_agent_tools/mcp/resources/register.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/resources/register.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/security/auth.d.ts +0 -24
- package/dist/hoi4_agent_tools/mcp/security/auth.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/security/auth.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/server/base-tools.d.ts +0 -47
- package/dist/hoi4_agent_tools/mcp/server/base-tools.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/server/base-tools.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/server/create.d.ts +0 -5
- package/dist/hoi4_agent_tools/mcp/server/create.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/server/create.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/server/output-schemas.d.ts +0 -138
- package/dist/hoi4_agent_tools/mcp/server/output-schemas.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/server/output-schemas.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/server/progress.d.ts +0 -8
- package/dist/hoi4_agent_tools/mcp/server/progress.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/server/progress.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/server/result.d.ts +0 -232
- package/dist/hoi4_agent_tools/mcp/server/result.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/server/result.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/server/transaction-execution.d.ts +0 -31
- package/dist/hoi4_agent_tools/mcp/server/transaction-execution.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/tools/focus.d.ts +0 -5
- package/dist/hoi4_agent_tools/mcp/tools/focus.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/tools/focus.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/tools/gui.d.ts +0 -117
- package/dist/hoi4_agent_tools/mcp/tools/gui.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/tools/gui.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/tools/map.d.ts +0 -5
- package/dist/hoi4_agent_tools/mcp/tools/map.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/tools/map.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.d.ts +0 -58
- package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/transports/event-store.d.ts +0 -26
- package/dist/hoi4_agent_tools/mcp/transports/event-store.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/transports/event-store.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/transports/http.d.ts +0 -28
- package/dist/hoi4_agent_tools/mcp/transports/http.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/transports/http.js.map +0 -1
- package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.d.ts +0 -15
- package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.js.map +0 -1
- package/dist/hoi4_agent_tools/runtime.d.ts +0 -4
- package/dist/hoi4_agent_tools/runtime.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/runtime.js.map +0 -1
- package/dist/hoi4_agent_tools/schemas/common.d.ts +0 -74
- package/dist/hoi4_agent_tools/schemas/common.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/schemas/common.js.map +0 -1
- package/dist/hoi4_agent_tools/schemas/focus.d.ts +0 -784
- package/dist/hoi4_agent_tools/schemas/focus.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/schemas/focus.js.map +0 -1
- package/dist/hoi4_agent_tools/schemas/index.d.ts +0 -5
- package/dist/hoi4_agent_tools/schemas/index.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/schemas/index.js +0 -5
- package/dist/hoi4_agent_tools/schemas/index.js.map +0 -1
- package/dist/hoi4_agent_tools/schemas/map.d.ts +0 -654
- package/dist/hoi4_agent_tools/schemas/map.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/schemas/map.js.map +0 -1
- package/dist/hoi4_agent_tools/schemas/transaction.d.ts +0 -259
- package/dist/hoi4_agent_tools/schemas/transaction.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/schemas/transaction.js.map +0 -1
- package/dist/hoi4_agent_tools/version.d.ts +0 -6
- package/dist/hoi4_agent_tools/version.d.ts.map +0 -1
- package/dist/hoi4_agent_tools/version.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +0 -1
- package/docs/adr/0001-runtime-and-language.md +0 -16
- package/docs/adr/0002-lossless-source-model.md +0 -18
- package/docs/adr/0003-image-and-font-codecs.md +0 -18
- package/docs/adr/0004-focus-layout.md +0 -16
- package/docs/adr/0005-offline-rendering.md +0 -18
- package/docs/adr/0006-mcp-sdk-and-protocol.md +0 -16
- package/docs/adr/0007-storage-and-transactions.md +0 -24
- package/docs/adr/0008-test-strategy.md +0 -18
- package/docs/adr/0009-packaging-and-registry.md +0 -16
- package/docs/adr/0010-unsupported-and-refusal-policy.md +0 -18
- package/docs/adr/0011-http-security.md +0 -16
- package/docs/adr/0012-autonomous-rewrites.md +0 -55
- package/docs/adr/README.md +0 -14
- package/docs/agent-integration.md +0 -146
- package/docs/architecture.md +0 -76
- package/docs/artifacts.md +0 -136
- package/docs/compatibility.md +0 -33
- package/docs/completion-report.md +0 -122
- package/docs/configuration.md +0 -305
- package/docs/focus-workflow.md +0 -197
- package/docs/gui-workflow.md +0 -125
- package/docs/limitations.md +0 -32
- package/docs/map-workflow.md +0 -95
- package/docs/performance.md +0 -117
- package/docs/release.md +0 -222
- package/docs/research/mcp-2026-07-10.md +0 -52
- package/docs/research/source-ledger.md +0 -68
- package/docs/security.md +0 -145
- package/docs/self-hosting.md +0 -170
- package/docs/testing.md +0 -43
- package/docs/third-party-notices.md +0 -11
- package/docs/transactions.md +0 -180
- package/docs/troubleshooting.md +0 -54
- package/schemas/configuration.schema.json +0 -795
- package/schemas/continuous-focus-palette.schema.json +0 -325
- package/schemas/focus-plan.schema.json +0 -927
- package/schemas/focus-planning-sidecar.schema.json +0 -379
- package/schemas/gui-animation-source.schema.json +0 -176
- package/schemas/gui-helper.schema.json +0 -301
- package/schemas/gui-scenario.schema.json +0 -298
- package/schemas/map-operation.schema.json +0 -2284
- package/schemas/operation-result.schema.json +0 -344
- package/schemas/transaction-manifest.schema.json +0 -685
|
@@ -3,18 +3,50 @@ import { compareCodeUnits, hashCanonical } from '../core/canonical.js';
|
|
|
3
3
|
import { DiagnosticCollector } from '../core/diagnostic-collector.js';
|
|
4
4
|
import { sortDiagnostics } from '../core/diagnostics.js';
|
|
5
5
|
import { ServiceError } from '../core/result.js';
|
|
6
|
-
import {
|
|
6
|
+
import { flattenFocusConnectorCurve, focusConnectorCurve, focusConnectorPolylinesIntersect, focusConnectorPolylineIntersectsRectangle, focusNodeRectangle, focusNodesVisiblyOverlap, } from './geometry.js';
|
|
7
7
|
import { FOCUS_GRAPH_MAX_DEPTH, FOCUS_DIAGNOSTIC_MAX, FOCUS_GRAPH_MAX_EDGES, FOCUS_GRAPH_MAX_NODES, FOCUS_LAYOUT_WORK_MAX, } from './limits.js';
|
|
8
8
|
import {} from './model.js';
|
|
9
|
-
class
|
|
9
|
+
export class FocusLayoutWorkBudget {
|
|
10
|
+
maximumWork;
|
|
10
11
|
used = 0;
|
|
12
|
+
constructor(maximumWork = FOCUS_LAYOUT_WORK_MAX) {
|
|
13
|
+
this.maximumWork = maximumWork;
|
|
14
|
+
if (!Number.isSafeInteger(maximumWork) || maximumWork <= 0) {
|
|
15
|
+
throw new ServiceError('FOCUS_LAYOUT_WORK_BUDGET_INVALID', 'Focus layout work budget must be a positive safe integer', { maximumWork });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
get consumed() {
|
|
19
|
+
return this.used;
|
|
20
|
+
}
|
|
11
21
|
spend(phase, amount = 1) {
|
|
12
|
-
if (!Number.isSafeInteger(amount) || amount < 0 || amount >
|
|
13
|
-
throw new ServiceError('FOCUS_LAYOUT_WORK_BUDGET_BLOCKED', 'Focus layout exceeds the fixed placement and comparison work ceiling', { phase, used: this.used, requested: amount, maximumWork:
|
|
22
|
+
if (!Number.isSafeInteger(amount) || amount < 0 || amount > this.maximumWork - this.used) {
|
|
23
|
+
throw new ServiceError('FOCUS_LAYOUT_WORK_BUDGET_BLOCKED', 'Focus layout exceeds the fixed placement and comparison work ceiling', { phase, used: this.used, requested: amount, maximumWork: this.maximumWork });
|
|
14
24
|
}
|
|
15
25
|
this.used += amount;
|
|
16
26
|
}
|
|
17
27
|
}
|
|
28
|
+
class FocusLayoutSubBudgetExhausted extends Error {
|
|
29
|
+
}
|
|
30
|
+
class FocusLayoutSubBudget {
|
|
31
|
+
delegate;
|
|
32
|
+
maximumWork;
|
|
33
|
+
startedAt;
|
|
34
|
+
constructor(delegate, maximumWork) {
|
|
35
|
+
this.delegate = delegate;
|
|
36
|
+
this.maximumWork = maximumWork;
|
|
37
|
+
this.startedAt = delegate.consumed;
|
|
38
|
+
}
|
|
39
|
+
get consumed() {
|
|
40
|
+
return this.delegate.consumed;
|
|
41
|
+
}
|
|
42
|
+
spend(phase, amount = 1) {
|
|
43
|
+
if (!Number.isSafeInteger(amount) ||
|
|
44
|
+
amount < 0 ||
|
|
45
|
+
this.delegate.consumed - this.startedAt + amount > this.maximumWork)
|
|
46
|
+
throw new FocusLayoutSubBudgetExhausted();
|
|
47
|
+
this.delegate.spend(`connector refinement: ${phase}`, amount);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
18
50
|
function* cancellationCheckpoint(signal, iteration = 0, stride = 1) {
|
|
19
51
|
if (iteration % stride !== 0)
|
|
20
52
|
return;
|
|
@@ -42,6 +74,11 @@ async function completeCooperatively(steps, signal) {
|
|
|
42
74
|
yieldDeadline = performance.now() + 8;
|
|
43
75
|
}
|
|
44
76
|
}
|
|
77
|
+
const DEFAULT_LANE_SPACING = 8;
|
|
78
|
+
const DEFAULT_NODE_SPACING = 2;
|
|
79
|
+
const LONG_CONNECTOR_HORIZONTAL_SPAN = 8;
|
|
80
|
+
const LONG_CONNECTOR_VERTICAL_SPAN = 4;
|
|
81
|
+
const LONG_CONNECTOR_MANHATTAN_SPAN = 10;
|
|
45
82
|
function coordinateKey(x, y) {
|
|
46
83
|
return `${x},${y}`;
|
|
47
84
|
}
|
|
@@ -62,9 +99,10 @@ function laneBases(plan, discovered, spacing) {
|
|
|
62
99
|
...configured.map(({ id }) => id),
|
|
63
100
|
...discovered.filter((id) => !configured.some((lane) => lane.id === id)),
|
|
64
101
|
];
|
|
65
|
-
|
|
102
|
+
const unique = [...new Set(all)];
|
|
103
|
+
return new Map(unique.map((id, index) => {
|
|
66
104
|
const lane = configured.find((candidate) => candidate.id === id);
|
|
67
|
-
const defaultX = index * spacing;
|
|
105
|
+
const defaultX = Math.round((index - (unique.length - 1) / 2) * spacing);
|
|
68
106
|
const bounded = Math.max(lane?.minimumX ?? -Infinity, Math.min(lane?.maximumX ?? Infinity, defaultX));
|
|
69
107
|
return [id, bounded];
|
|
70
108
|
}));
|
|
@@ -98,6 +136,71 @@ function finiteLaneBoundDetails(bounds) {
|
|
|
98
136
|
function prerequisiteIds(focus) {
|
|
99
137
|
return focus.prerequisites.groups.flatMap(({ focusIds }) => focusIds);
|
|
100
138
|
}
|
|
139
|
+
function automaticSiblingLayout(plan, focuses, laneIds, spacing) {
|
|
140
|
+
const placementCohorts = new Map();
|
|
141
|
+
const metricCohorts = new Map();
|
|
142
|
+
const configuredLaneOrder = new Map([...plan.laneGroups]
|
|
143
|
+
.sort((left, right) => left.order - right.order || compareCodeUnits(left.id, right.id))
|
|
144
|
+
.map((lane, index) => [lane.id, index]));
|
|
145
|
+
for (const focus of focuses.values()) {
|
|
146
|
+
if (focus.position.mode !== 'auto')
|
|
147
|
+
continue;
|
|
148
|
+
const parents = [...prerequisiteIds(focus)].sort((left, right) => compareCodeUnits(left, right));
|
|
149
|
+
const placementKey = JSON.stringify([
|
|
150
|
+
parents.length === 0 ? (laneIds.get(focus.id) ?? 'default') : null,
|
|
151
|
+
parents,
|
|
152
|
+
focus.position.preferredX ?? null,
|
|
153
|
+
focus.position.preferredY ?? null,
|
|
154
|
+
]);
|
|
155
|
+
const metricKey = JSON.stringify([
|
|
156
|
+
parents.length === 0 ? (laneIds.get(focus.id) ?? 'default') : null,
|
|
157
|
+
parents,
|
|
158
|
+
focus.position.preferredY ?? null,
|
|
159
|
+
]);
|
|
160
|
+
const placementCohort = placementCohorts.get(placementKey) ?? [];
|
|
161
|
+
placementCohort.push(focus.id);
|
|
162
|
+
placementCohorts.set(placementKey, placementCohort);
|
|
163
|
+
const metricCohort = metricCohorts.get(metricKey) ?? [];
|
|
164
|
+
metricCohort.push(focus.id);
|
|
165
|
+
metricCohorts.set(metricKey, metricCohort);
|
|
166
|
+
}
|
|
167
|
+
const result = new Map();
|
|
168
|
+
for (const cohort of placementCohorts.values()) {
|
|
169
|
+
cohort.sort((left, right) => {
|
|
170
|
+
const leftLane = laneIds.get(left) ?? 'default';
|
|
171
|
+
const rightLane = laneIds.get(right) ?? 'default';
|
|
172
|
+
return ((configuredLaneOrder.get(leftLane) ?? Number.MAX_SAFE_INTEGER) -
|
|
173
|
+
(configuredLaneOrder.get(rightLane) ?? Number.MAX_SAFE_INTEGER) ||
|
|
174
|
+
compareCodeUnits(leftLane, rightLane) ||
|
|
175
|
+
compareCodeUnits(left, right));
|
|
176
|
+
});
|
|
177
|
+
for (const [index, focusId] of cohort.entries()) {
|
|
178
|
+
// Integer HOI4 coordinates cannot center an even cohort on an integer
|
|
179
|
+
// column. Floor keeps exact mirror spacing around the adjacent half-grid.
|
|
180
|
+
result.set(focusId, Math.floor(((2 * index - (cohort.length - 1)) * spacing) / 2));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const cohort of metricCohorts.values()) {
|
|
184
|
+
cohort.sort((left, right) => compareCodeUnits(left, right));
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
offsets: result,
|
|
188
|
+
cohorts: [...metricCohorts.values()].filter((cohort) => cohort.length > 1),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function structuralAnchorX(context, focus, parents, laneId) {
|
|
192
|
+
if (parents.length > 1 && focus.convergence) {
|
|
193
|
+
return Math.floor(parents.reduce((total, parent) => total + parent.x, 0) / parents.length);
|
|
194
|
+
}
|
|
195
|
+
if (parents.length > 0) {
|
|
196
|
+
const ordered = parents.map(({ x }) => x).sort((left, right) => left - right);
|
|
197
|
+
const middle = Math.floor(ordered.length / 2);
|
|
198
|
+
if (ordered.length % 2 === 1)
|
|
199
|
+
return ordered[middle] ?? 0;
|
|
200
|
+
return Math.floor(((ordered[middle - 1] ?? 0) + (ordered[middle] ?? 0)) / 2);
|
|
201
|
+
}
|
|
202
|
+
return context.laneBases.get(laneId) ?? 0;
|
|
203
|
+
}
|
|
101
204
|
function mutualExclusionMap(focuses) {
|
|
102
205
|
const result = new Map();
|
|
103
206
|
for (const focus of focuses.values()) {
|
|
@@ -148,25 +251,44 @@ function connectorsShareEndpoint(left, right) {
|
|
|
148
251
|
left.childId === right.parentId ||
|
|
149
252
|
left.childId === right.childId);
|
|
150
253
|
}
|
|
254
|
+
function connectorVerticalInteriorsOverlap(left, right) {
|
|
255
|
+
const leftMinimum = Math.min(left.parent.y, left.child.y);
|
|
256
|
+
const leftMaximum = Math.max(left.parent.y, left.child.y);
|
|
257
|
+
const rightMinimum = Math.min(right.parent.y, right.child.y);
|
|
258
|
+
const rightMaximum = Math.max(right.parent.y, right.child.y);
|
|
259
|
+
return Math.max(leftMinimum, rightMinimum) < Math.min(leftMaximum, rightMaximum);
|
|
260
|
+
}
|
|
151
261
|
function* connectorCrossings(edges, context) {
|
|
152
262
|
const crossings = [];
|
|
153
|
-
|
|
263
|
+
const flattened = edges.map((edge) => ({
|
|
264
|
+
edge,
|
|
265
|
+
points: flattenFocusConnectorCurve(focusConnectorCurve(edge.parent, edge.child)),
|
|
266
|
+
}));
|
|
267
|
+
let crossingCount = 0;
|
|
268
|
+
for (let leftIndex = 0; leftIndex < flattened.length; leftIndex += 1) {
|
|
154
269
|
yield* cancellationCheckpoint(context.signal, leftIndex, 16);
|
|
155
|
-
const left =
|
|
270
|
+
const left = flattened[leftIndex];
|
|
156
271
|
if (left === undefined)
|
|
157
272
|
continue;
|
|
158
|
-
for (let rightIndex = leftIndex + 1; rightIndex <
|
|
273
|
+
for (let rightIndex = leftIndex + 1; rightIndex < flattened.length; rightIndex += 1) {
|
|
159
274
|
yield* cancellationCheckpoint(context.signal, rightIndex - leftIndex, 64);
|
|
160
|
-
context.work.spend('connector crossing comparison');
|
|
161
275
|
const right = edges[rightIndex];
|
|
162
|
-
|
|
276
|
+
const rightFlattened = flattened[rightIndex];
|
|
277
|
+
if (right === undefined || rightFlattened === undefined)
|
|
278
|
+
continue;
|
|
279
|
+
context.work.spend('connector pair examination');
|
|
280
|
+
if (connectorsShareEndpoint(left.edge, right))
|
|
163
281
|
continue;
|
|
164
|
-
if (
|
|
282
|
+
if (!connectorVerticalInteriorsOverlap(left.edge, right))
|
|
283
|
+
continue;
|
|
284
|
+
if (focusConnectorPolylinesIntersect(left.points, rightFlattened.points)) {
|
|
285
|
+
crossingCount += 1;
|
|
165
286
|
if (crossings.length < FOCUS_DIAGNOSTIC_MAX - 1)
|
|
166
|
-
crossings.push([left, right]);
|
|
287
|
+
crossings.push([left.edge, right]);
|
|
288
|
+
}
|
|
167
289
|
}
|
|
168
290
|
}
|
|
169
|
-
return crossings;
|
|
291
|
+
return { count: crossingCount, samples: crossings };
|
|
170
292
|
}
|
|
171
293
|
function* crossingCountForCandidate(context, focusId, x, y, stopAt = Infinity) {
|
|
172
294
|
yield* cancellationCheckpoint(context.signal);
|
|
@@ -193,18 +315,32 @@ function* crossingCountForCandidate(context, focusId, x, y, stopAt = Infinity) {
|
|
|
193
315
|
});
|
|
194
316
|
}
|
|
195
317
|
}
|
|
196
|
-
|
|
318
|
+
// Snapshot once so every incident candidate edge is compared. Reusing a
|
|
319
|
+
// Map iterator here silently exhausted it after the first incident edge.
|
|
320
|
+
const existingEdges = [...context.placedConnectors.values()];
|
|
321
|
+
const flattenedCandidates = candidateEdges.map((edge) => ({
|
|
322
|
+
edge,
|
|
323
|
+
points: flattenFocusConnectorCurve(focusConnectorCurve(edge.parent, edge.child)),
|
|
324
|
+
}));
|
|
325
|
+
const flattenedExisting = existingEdges.map((edge) => ({
|
|
326
|
+
edge,
|
|
327
|
+
points: flattenFocusConnectorCurve(focusConnectorCurve(edge.parent, edge.child)),
|
|
328
|
+
}));
|
|
197
329
|
let crossings = 0;
|
|
198
|
-
for (const [candidateIndex,
|
|
330
|
+
for (const [candidateIndex, candidate] of flattenedCandidates.entries()) {
|
|
199
331
|
yield* cancellationCheckpoint(context.signal, candidateIndex, 16);
|
|
200
332
|
let existingIndex = 0;
|
|
201
|
-
for (const
|
|
333
|
+
for (const existing of flattenedExisting) {
|
|
202
334
|
yield* cancellationCheckpoint(context.signal, existingIndex, 64);
|
|
203
335
|
existingIndex += 1;
|
|
204
|
-
|
|
336
|
+
const candidateEdge = candidate.edge;
|
|
337
|
+
const existingEdge = existing.edge;
|
|
338
|
+
context.work.spend('candidate connector pair examination');
|
|
205
339
|
if (connectorsShareEndpoint(candidateEdge, existingEdge))
|
|
206
340
|
continue;
|
|
207
|
-
if (
|
|
341
|
+
if (!connectorVerticalInteriorsOverlap(candidateEdge, existingEdge))
|
|
342
|
+
continue;
|
|
343
|
+
if (focusConnectorPolylinesIntersect(candidate.points, existing.points)) {
|
|
208
344
|
crossings += 1;
|
|
209
345
|
if (crossings >= stopAt)
|
|
210
346
|
return crossings;
|
|
@@ -213,6 +349,16 @@ function* crossingCountForCandidate(context, focusId, x, y, stopAt = Infinity) {
|
|
|
213
349
|
}
|
|
214
350
|
return crossings;
|
|
215
351
|
}
|
|
352
|
+
function horizontalSpanForCandidate(context, focusId, x) {
|
|
353
|
+
let span = 0;
|
|
354
|
+
for (const definition of context.connectorDefinitionsByFocus.get(focusId) ?? []) {
|
|
355
|
+
const otherId = definition.parentId === focusId ? definition.childId : definition.parentId;
|
|
356
|
+
const other = context.placed.get(otherId);
|
|
357
|
+
if (other !== undefined)
|
|
358
|
+
span += Math.abs(x - other.x);
|
|
359
|
+
}
|
|
360
|
+
return span;
|
|
361
|
+
}
|
|
216
362
|
function candidateConflicts(context, focusId, x, y) {
|
|
217
363
|
context.work.spend('candidate overlap lookup');
|
|
218
364
|
// Focus coordinates are integers and the rendered node is smaller than one
|
|
@@ -221,6 +367,19 @@ function candidateConflicts(context, focusId, x, y) {
|
|
|
221
367
|
// placed node for every automatic candidate.
|
|
222
368
|
const occupiedBy = context.occupied.get(coordinateKey(x, y));
|
|
223
369
|
const visibleOverlaps = occupiedBy === undefined || occupiedBy === focusId ? [] : [occupiedBy];
|
|
370
|
+
const focusSpacing = [];
|
|
371
|
+
const occupiedRow = context.occupiedRows.get(y);
|
|
372
|
+
if (occupiedRow !== undefined) {
|
|
373
|
+
for (let offset = 1; offset < context.nodeSpacing; offset += 1) {
|
|
374
|
+
context.work.spend('candidate same-row spacing lookup', 2);
|
|
375
|
+
for (const candidateX of [x - offset, x + offset]) {
|
|
376
|
+
const neighbor = occupiedRow.get(candidateX);
|
|
377
|
+
if (neighbor !== undefined && neighbor !== focusId)
|
|
378
|
+
focusSpacing.push(neighbor);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
focusSpacing.sort((left, right) => compareCodeUnits(left, right));
|
|
224
383
|
const mutualExclusions = [];
|
|
225
384
|
for (const otherId of context.mutualExclusions.get(focusId) ?? []) {
|
|
226
385
|
context.work.spend('candidate mutual-exclusion comparison');
|
|
@@ -229,10 +388,10 @@ function candidateConflicts(context, focusId, x, y) {
|
|
|
229
388
|
mutualExclusions.push(otherId);
|
|
230
389
|
}
|
|
231
390
|
mutualExclusions.sort((left, right) => compareCodeUnits(left, right));
|
|
232
|
-
return { visibleOverlaps, mutualExclusions };
|
|
391
|
+
return { visibleOverlaps, focusSpacing, mutualExclusions };
|
|
233
392
|
}
|
|
234
|
-
function conflictsCandidate({ visibleOverlaps, mutualExclusions }) {
|
|
235
|
-
return visibleOverlaps.length > 0 || mutualExclusions.length > 0;
|
|
393
|
+
function conflictsCandidate({ visibleOverlaps, focusSpacing, mutualExclusions, }) {
|
|
394
|
+
return visibleOverlaps.length > 0 || focusSpacing.length > 0 || mutualExclusions.length > 0;
|
|
236
395
|
}
|
|
237
396
|
function* placedPrerequisites(context, focus, stack) {
|
|
238
397
|
const result = [];
|
|
@@ -259,6 +418,9 @@ function recordOccupied(context, node) {
|
|
|
259
418
|
}
|
|
260
419
|
else {
|
|
261
420
|
context.occupied.set(key, node.id);
|
|
421
|
+
const occupiedRow = context.occupiedRows.get(node.y) ?? new Map();
|
|
422
|
+
occupiedRow.set(node.x, node.id);
|
|
423
|
+
context.occupiedRows.set(node.y, occupiedRow);
|
|
262
424
|
}
|
|
263
425
|
}
|
|
264
426
|
function recordPlaced(context, node) {
|
|
@@ -278,6 +440,28 @@ function recordPlaced(context, node) {
|
|
|
278
440
|
});
|
|
279
441
|
}
|
|
280
442
|
}
|
|
443
|
+
function movePlacedFocusX(context, node, x) {
|
|
444
|
+
context.occupied.delete(coordinateKey(node.x, node.y));
|
|
445
|
+
const oldRow = context.occupiedRows.get(node.y);
|
|
446
|
+
oldRow?.delete(node.x);
|
|
447
|
+
if (oldRow?.size === 0)
|
|
448
|
+
context.occupiedRows.delete(node.y);
|
|
449
|
+
node.x = x;
|
|
450
|
+
context.occupied.set(coordinateKey(node.x, node.y), node.id);
|
|
451
|
+
const newRow = context.occupiedRows.get(node.y) ?? new Map();
|
|
452
|
+
newRow.set(node.x, node.id);
|
|
453
|
+
context.occupiedRows.set(node.y, newRow);
|
|
454
|
+
}
|
|
455
|
+
function rebuildOccupancy(context) {
|
|
456
|
+
context.occupied.clear();
|
|
457
|
+
context.occupiedRows.clear();
|
|
458
|
+
for (const node of context.placed.values()) {
|
|
459
|
+
context.occupied.set(coordinateKey(node.x, node.y), node.id);
|
|
460
|
+
const row = context.occupiedRows.get(node.y) ?? new Map();
|
|
461
|
+
row.set(node.x, node.id);
|
|
462
|
+
context.occupiedRows.set(node.y, row);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
281
465
|
function* availableX(context, focusId, preferred, y) {
|
|
282
466
|
const conflictsAtPreferred = candidateConflicts(context, focusId, preferred, y);
|
|
283
467
|
const crossingsAtPreferred = yield* crossingCountForCandidate(context, focusId, preferred, y);
|
|
@@ -292,7 +476,12 @@ function* availableX(context, focusId, preferred, y) {
|
|
|
292
476
|
}
|
|
293
477
|
let best = conflictsCandidate(conflictsAtPreferred)
|
|
294
478
|
? undefined
|
|
295
|
-
: {
|
|
479
|
+
: {
|
|
480
|
+
x: preferred,
|
|
481
|
+
crossings: crossingsAtPreferred,
|
|
482
|
+
distance: 0,
|
|
483
|
+
horizontalSpan: horizontalSpanForCandidate(context, focusId, preferred),
|
|
484
|
+
};
|
|
296
485
|
// Crossing optimization is deliberately local to the declared lane or
|
|
297
486
|
// preferred coordinate. Scaling the radius with total tree size made one
|
|
298
487
|
// unavoidable crossing consume the entire global work budget on large
|
|
@@ -302,11 +491,8 @@ function* availableX(context, focusId, preferred, y) {
|
|
|
302
491
|
const optimizationSteps = 32;
|
|
303
492
|
for (let distance = 1; distance <= optimizationSteps; distance += 1) {
|
|
304
493
|
yield* cancellationCheckpoint(context.signal, distance, 8);
|
|
305
|
-
const offsets = [distance,
|
|
306
|
-
for (
|
|
307
|
-
const offset = offsets[directionOrder];
|
|
308
|
-
if (offset === undefined)
|
|
309
|
-
continue;
|
|
494
|
+
const offsets = [-distance, distance];
|
|
495
|
+
for (const offset of offsets) {
|
|
310
496
|
context.work.spend('automatic placement candidate');
|
|
311
497
|
const x = preferred + offset * context.nodeSpacing;
|
|
312
498
|
if (!coordinateWithinLane(context, focusId, x))
|
|
@@ -315,24 +501,34 @@ function* availableX(context, focusId, preferred, y) {
|
|
|
315
501
|
if (conflictsCandidate(conflicts))
|
|
316
502
|
continue;
|
|
317
503
|
const crossings = yield* crossingCountForCandidate(context, focusId, x, y, best?.crossings);
|
|
504
|
+
const horizontalSpan = horizontalSpanForCandidate(context, focusId, x);
|
|
318
505
|
if (best === undefined ||
|
|
319
506
|
crossings < best.crossings ||
|
|
320
507
|
(crossings === best.crossings && distance < best.distance) ||
|
|
321
508
|
(crossings === best.crossings &&
|
|
322
509
|
distance === best.distance &&
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
};
|
|
510
|
+
horizontalSpan < best.horizontalSpan) ||
|
|
511
|
+
(crossings === best.crossings &&
|
|
512
|
+
distance === best.distance &&
|
|
513
|
+
horizontalSpan === best.horizontalSpan &&
|
|
514
|
+
Math.abs(x) < Math.abs(best.x)) ||
|
|
515
|
+
(crossings === best.crossings &&
|
|
516
|
+
distance === best.distance &&
|
|
517
|
+
horizontalSpan === best.horizontalSpan &&
|
|
518
|
+
Math.abs(x) === Math.abs(best.x) &&
|
|
519
|
+
x < best.x)) {
|
|
520
|
+
best = { x, crossings, distance, horizontalSpan };
|
|
334
521
|
}
|
|
335
522
|
}
|
|
523
|
+
if (best?.crossings === 0 && best.distance === distance) {
|
|
524
|
+
return {
|
|
525
|
+
x: best.x,
|
|
526
|
+
moved: best.x !== preferred,
|
|
527
|
+
conflictsAtPreferred,
|
|
528
|
+
crossingsAtPreferred,
|
|
529
|
+
crossings: best.crossings,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
336
532
|
}
|
|
337
533
|
if (best !== undefined) {
|
|
338
534
|
return {
|
|
@@ -345,7 +541,7 @@ function* availableX(context, focusId, preferred, y) {
|
|
|
345
541
|
}
|
|
346
542
|
for (let distance = optimizationSteps + 1; distance < 10_000; distance += 1) {
|
|
347
543
|
yield* cancellationCheckpoint(context.signal, distance, 16);
|
|
348
|
-
for (const offset of [distance,
|
|
544
|
+
for (const offset of [-distance, distance]) {
|
|
349
545
|
context.work.spend('automatic placement fallback candidate');
|
|
350
546
|
const x = preferred + offset * context.nodeSpacing;
|
|
351
547
|
if (!coordinateWithinLane(context, focusId, x))
|
|
@@ -365,6 +561,336 @@ function* availableX(context, focusId, preferred, y) {
|
|
|
365
561
|
const bounds = boundsForFocus(context, focusId);
|
|
366
562
|
throw new ServiceError('FOCUS_LAYOUT_LANE_CAPACITY_BLOCKED', 'No collision-free coordinate is available inside the focus lane bounds', { focusId, laneId, ...finiteLaneBoundDetails(bounds) });
|
|
367
563
|
}
|
|
564
|
+
function betterGlobalLayoutQualityScore(candidate, current) {
|
|
565
|
+
return (candidate.crossingCount < current.crossingCount ||
|
|
566
|
+
(candidate.crossingCount === current.crossingCount &&
|
|
567
|
+
candidate.maximumHorizontalSpan < current.maximumHorizontalSpan) ||
|
|
568
|
+
(candidate.crossingCount === current.crossingCount &&
|
|
569
|
+
candidate.maximumHorizontalSpan === current.maximumHorizontalSpan &&
|
|
570
|
+
candidate.columnCount < current.columnCount) ||
|
|
571
|
+
(candidate.crossingCount === current.crossingCount &&
|
|
572
|
+
candidate.maximumHorizontalSpan === current.maximumHorizontalSpan &&
|
|
573
|
+
candidate.columnCount === current.columnCount &&
|
|
574
|
+
candidate.totalHorizontalSpan < current.totalHorizontalSpan) ||
|
|
575
|
+
(candidate.crossingCount === current.crossingCount &&
|
|
576
|
+
candidate.maximumHorizontalSpan === current.maximumHorizontalSpan &&
|
|
577
|
+
candidate.columnCount === current.columnCount &&
|
|
578
|
+
candidate.totalHorizontalSpan === current.totalHorizontalSpan &&
|
|
579
|
+
candidate.boundingCenterOffsetTwice < current.boundingCenterOffsetTwice));
|
|
580
|
+
}
|
|
581
|
+
function candidateConnector(context, definition, candidates) {
|
|
582
|
+
const placedParent = context.placed.get(definition.parentId);
|
|
583
|
+
const placedChild = context.placed.get(definition.childId);
|
|
584
|
+
if (placedParent === undefined || placedChild === undefined)
|
|
585
|
+
return undefined;
|
|
586
|
+
const parentX = candidates.get(definition.parentId);
|
|
587
|
+
const childX = candidates.get(definition.childId);
|
|
588
|
+
return {
|
|
589
|
+
parentId: definition.parentId,
|
|
590
|
+
childId: definition.childId,
|
|
591
|
+
parent: parentX === undefined ? placedParent : { ...placedParent, x: parentX },
|
|
592
|
+
child: childX === undefined ? placedChild : { ...placedChild, x: childX },
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function* crossingCountForCandidateSet(context, candidates, stopAt = Infinity) {
|
|
596
|
+
const definitions = new Map();
|
|
597
|
+
for (const focusId of candidates.keys()) {
|
|
598
|
+
for (const definition of context.connectorDefinitionsByFocus.get(focusId) ?? []) {
|
|
599
|
+
definitions.set(definition.key, definition);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const candidateEdges = [...definitions]
|
|
603
|
+
.sort((left, right) => left[0] - right[0])
|
|
604
|
+
.flatMap(([_key, definition]) => {
|
|
605
|
+
const edge = candidateConnector(context, definition, candidates);
|
|
606
|
+
return edge === undefined ? [] : [edge];
|
|
607
|
+
});
|
|
608
|
+
const candidateIds = new Set(candidates.keys());
|
|
609
|
+
const unaffectedEdges = [...context.placedConnectors.values()].filter((edge) => !candidateIds.has(edge.parentId) && !candidateIds.has(edge.childId));
|
|
610
|
+
const flattenedCandidates = candidateEdges.map((edge) => ({
|
|
611
|
+
edge,
|
|
612
|
+
points: flattenFocusConnectorCurve(focusConnectorCurve(edge.parent, edge.child)),
|
|
613
|
+
}));
|
|
614
|
+
const flattenedUnaffected = unaffectedEdges.map((edge) => ({
|
|
615
|
+
edge,
|
|
616
|
+
points: flattenFocusConnectorCurve(focusConnectorCurve(edge.parent, edge.child)),
|
|
617
|
+
}));
|
|
618
|
+
let crossings = 0;
|
|
619
|
+
for (const [candidateIndex, candidate] of flattenedCandidates.entries()) {
|
|
620
|
+
yield* cancellationCheckpoint(context.signal, candidateIndex, 8);
|
|
621
|
+
for (const unaffected of flattenedUnaffected) {
|
|
622
|
+
const edge = candidate.edge;
|
|
623
|
+
const unaffectedEdge = unaffected.edge;
|
|
624
|
+
context.work.spend('paired candidate connector pair examination');
|
|
625
|
+
if (connectorsShareEndpoint(edge, unaffectedEdge))
|
|
626
|
+
continue;
|
|
627
|
+
if (!connectorVerticalInteriorsOverlap(edge, unaffectedEdge))
|
|
628
|
+
continue;
|
|
629
|
+
if (!focusConnectorPolylinesIntersect(candidate.points, unaffected.points))
|
|
630
|
+
continue;
|
|
631
|
+
crossings += 1;
|
|
632
|
+
if (crossings >= stopAt)
|
|
633
|
+
return crossings;
|
|
634
|
+
}
|
|
635
|
+
for (let otherIndex = candidateIndex + 1; otherIndex < flattenedCandidates.length; otherIndex += 1) {
|
|
636
|
+
const other = flattenedCandidates[otherIndex];
|
|
637
|
+
context.work.spend('paired candidate internal pair examination');
|
|
638
|
+
if (other === undefined ||
|
|
639
|
+
connectorsShareEndpoint(candidate.edge, other.edge) ||
|
|
640
|
+
!connectorVerticalInteriorsOverlap(candidate.edge, other.edge))
|
|
641
|
+
continue;
|
|
642
|
+
if (!focusConnectorPolylinesIntersect(candidate.points, other.points))
|
|
643
|
+
continue;
|
|
644
|
+
crossings += 1;
|
|
645
|
+
if (crossings >= stopAt)
|
|
646
|
+
return crossings;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return crossings;
|
|
650
|
+
}
|
|
651
|
+
function candidateSetConflicts(context, candidates) {
|
|
652
|
+
const candidateIds = new Set(candidates.keys());
|
|
653
|
+
for (const [focusId, x] of candidates) {
|
|
654
|
+
if (!coordinateWithinLane(context, focusId, x))
|
|
655
|
+
return true;
|
|
656
|
+
const node = context.placed.get(focusId);
|
|
657
|
+
if (node === undefined)
|
|
658
|
+
return true;
|
|
659
|
+
const row = context.occupiedRows.get(node.y);
|
|
660
|
+
if (row !== undefined) {
|
|
661
|
+
for (let offset = 0; offset < context.nodeSpacing; offset += 1) {
|
|
662
|
+
for (const otherX of offset === 0 ? [x] : [x - offset, x + offset]) {
|
|
663
|
+
const otherId = row.get(otherX);
|
|
664
|
+
if (otherId !== undefined && !candidateIds.has(otherId))
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
for (const excludedId of context.mutualExclusions.get(focusId) ?? []) {
|
|
670
|
+
const excludedX = candidates.get(excludedId) ?? context.placed.get(excludedId)?.x;
|
|
671
|
+
if (excludedX !== undefined &&
|
|
672
|
+
Math.abs(x - excludedX) < context.minimumMutualExclusionSpacing)
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const entries = [...candidates];
|
|
677
|
+
for (let leftIndex = 0; leftIndex < entries.length; leftIndex += 1) {
|
|
678
|
+
const [leftId, leftX] = entries[leftIndex] ?? [];
|
|
679
|
+
const left = leftId === undefined ? undefined : context.placed.get(leftId);
|
|
680
|
+
if (leftId === undefined || leftX === undefined || left === undefined)
|
|
681
|
+
continue;
|
|
682
|
+
for (let rightIndex = leftIndex + 1; rightIndex < entries.length; rightIndex += 1) {
|
|
683
|
+
const [rightId, rightX] = entries[rightIndex] ?? [];
|
|
684
|
+
const right = rightId === undefined ? undefined : context.placed.get(rightId);
|
|
685
|
+
if (rightId === undefined || rightX === undefined || right === undefined)
|
|
686
|
+
continue;
|
|
687
|
+
if (left.y === right.y && Math.abs(leftX - rightX) < context.nodeSpacing)
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
function* repairConnectorCrossings(context) {
|
|
694
|
+
const maximumAcceptedMoves = 10;
|
|
695
|
+
const maximumDistance = 24;
|
|
696
|
+
const maximumPairDistance = 8;
|
|
697
|
+
const maximumPairCandidates = 1_000;
|
|
698
|
+
const maximumRepairWork = 500_000;
|
|
699
|
+
const repairStartedAt = context.work.consumed;
|
|
700
|
+
let pairCandidates = 0;
|
|
701
|
+
for (let acceptedMoves = 0; acceptedMoves < maximumAcceptedMoves; acceptedMoves += 1) {
|
|
702
|
+
if (context.work.consumed - repairStartedAt >= maximumRepairWork)
|
|
703
|
+
return;
|
|
704
|
+
yield* cancellationCheckpoint(context.signal, acceptedMoves);
|
|
705
|
+
const edges = yield* connectorEdges(context);
|
|
706
|
+
const crossingSummary = yield* connectorCrossings(edges, context);
|
|
707
|
+
if (crossingSummary.count === 0)
|
|
708
|
+
return;
|
|
709
|
+
const horizontalSpans = edges.map((edge) => ({ edge, span: connectorSpans(edge).horizontal }));
|
|
710
|
+
const totalHorizontalSpan = horizontalSpans.reduce((total, entry) => total + entry.span, 0);
|
|
711
|
+
const maximumHorizontalSpan = horizontalSpans.reduce((maximum, entry) => Math.max(maximum, entry.span), 0);
|
|
712
|
+
const nodes = [...context.placed.values()];
|
|
713
|
+
const minimumX = Math.min(...nodes.map(({ x }) => x));
|
|
714
|
+
const maximumX = Math.max(...nodes.map(({ x }) => x));
|
|
715
|
+
const currentScore = {
|
|
716
|
+
crossingCount: crossingSummary.count,
|
|
717
|
+
maximumHorizontalSpan,
|
|
718
|
+
columnCount: maximumX - minimumX + 1,
|
|
719
|
+
totalHorizontalSpan,
|
|
720
|
+
boundingCenterOffsetTwice: Math.abs(minimumX + maximumX),
|
|
721
|
+
};
|
|
722
|
+
const endpointIds = [
|
|
723
|
+
...new Set(crossingSummary.samples.flatMap(([left, right]) => [
|
|
724
|
+
left.parentId,
|
|
725
|
+
left.childId,
|
|
726
|
+
right.parentId,
|
|
727
|
+
right.childId,
|
|
728
|
+
])),
|
|
729
|
+
].sort((left, right) => compareCodeUnits(left, right));
|
|
730
|
+
let accepted = false;
|
|
731
|
+
for (const [focusIndex, focusId] of endpointIds.entries()) {
|
|
732
|
+
yield* cancellationCheckpoint(context.signal, focusIndex, 8);
|
|
733
|
+
if (!canMoveForSoftConstraint(context, focusId))
|
|
734
|
+
continue;
|
|
735
|
+
const node = context.placed.get(focusId);
|
|
736
|
+
if (node === undefined)
|
|
737
|
+
continue;
|
|
738
|
+
const incident = horizontalSpans.filter(({ edge }) => edge.parentId === focusId || edge.childId === focusId);
|
|
739
|
+
const nonIncident = horizontalSpans.filter(({ edge }) => edge.parentId !== focusId && edge.childId !== focusId);
|
|
740
|
+
const currentIncidentCrossings = yield* crossingCountForCandidate(context, focusId, node.x, node.y);
|
|
741
|
+
const currentIncidentHorizontalSpan = incident.reduce((total, entry) => total + entry.span, 0);
|
|
742
|
+
const maximumNonIncidentHorizontalSpan = nonIncident.reduce((maximum, entry) => Math.max(maximum, entry.span), 0);
|
|
743
|
+
const otherXCoordinates = nodes.filter(({ id }) => id !== focusId).map(({ x }) => x);
|
|
744
|
+
const minimumOtherX = Math.min(...otherXCoordinates);
|
|
745
|
+
const maximumOtherX = Math.max(...otherXCoordinates);
|
|
746
|
+
context.work.spend('global crossing repair focus snapshot', horizontalSpans.length + nodes.length);
|
|
747
|
+
for (let distance = 1; distance <= maximumDistance && !accepted; distance += 1) {
|
|
748
|
+
if (context.work.consumed - repairStartedAt >= maximumRepairWork)
|
|
749
|
+
return;
|
|
750
|
+
for (const x of [node.x - distance, node.x + distance]) {
|
|
751
|
+
context.work.spend('global crossing repair candidate');
|
|
752
|
+
if (!coordinateWithinLane(context, focusId, x))
|
|
753
|
+
continue;
|
|
754
|
+
if (conflictsCandidate(candidateConflicts(context, focusId, x, node.y)))
|
|
755
|
+
continue;
|
|
756
|
+
const candidateIncidentCrossings = yield* crossingCountForCandidate(context, focusId, x, node.y, currentIncidentCrossings + 1);
|
|
757
|
+
if (candidateIncidentCrossings > currentIncidentCrossings)
|
|
758
|
+
continue;
|
|
759
|
+
const candidateIncidentSpans = incident.map(({ edge }) => {
|
|
760
|
+
const other = edge.parentId === focusId ? edge.child : edge.parent;
|
|
761
|
+
return Math.abs(x - other.x);
|
|
762
|
+
});
|
|
763
|
+
const candidateMinimumX = Math.min(minimumOtherX, x);
|
|
764
|
+
const candidateMaximumX = Math.max(maximumOtherX, x);
|
|
765
|
+
const candidateScore = {
|
|
766
|
+
crossingCount: crossingSummary.count - currentIncidentCrossings + candidateIncidentCrossings,
|
|
767
|
+
maximumHorizontalSpan: Math.max(maximumNonIncidentHorizontalSpan, ...candidateIncidentSpans),
|
|
768
|
+
columnCount: candidateMaximumX - candidateMinimumX + 1,
|
|
769
|
+
totalHorizontalSpan: totalHorizontalSpan -
|
|
770
|
+
currentIncidentHorizontalSpan +
|
|
771
|
+
candidateIncidentSpans.reduce((total, span) => total + span, 0),
|
|
772
|
+
boundingCenterOffsetTwice: Math.abs(candidateMinimumX + candidateMaximumX),
|
|
773
|
+
};
|
|
774
|
+
if (!betterGlobalLayoutQualityScore(candidateScore, currentScore))
|
|
775
|
+
continue;
|
|
776
|
+
context.decisions.push({
|
|
777
|
+
focusId,
|
|
778
|
+
kind: 'moved_to_reduce_crossings',
|
|
779
|
+
message: `Global crossing repair moved (${node.x}, ${node.y}) to (${x}, ${node.y}); crossings ${currentScore.crossingCount} -> ${candidateScore.crossingCount}`,
|
|
780
|
+
});
|
|
781
|
+
movePlacedFocusX(context, node, x);
|
|
782
|
+
accepted = true;
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (accepted)
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
for (let leftIndex = 0; leftIndex < endpointIds.length && !accepted; leftIndex += 1) {
|
|
790
|
+
const leftId = endpointIds[leftIndex];
|
|
791
|
+
if (leftId === undefined || !canMoveForSoftConstraint(context, leftId))
|
|
792
|
+
continue;
|
|
793
|
+
const leftNode = context.placed.get(leftId);
|
|
794
|
+
if (leftNode === undefined)
|
|
795
|
+
continue;
|
|
796
|
+
for (let rightIndex = leftIndex + 1; rightIndex < endpointIds.length && !accepted; rightIndex += 1) {
|
|
797
|
+
const rightId = endpointIds[rightIndex];
|
|
798
|
+
if (rightId === undefined || !canMoveForSoftConstraint(context, rightId))
|
|
799
|
+
continue;
|
|
800
|
+
const rightNode = context.placed.get(rightId);
|
|
801
|
+
if (rightNode === undefined)
|
|
802
|
+
continue;
|
|
803
|
+
const focusIds = new Set([leftId, rightId]);
|
|
804
|
+
const incidentEdges = edges.filter((edge) => focusIds.has(edge.parentId) || focusIds.has(edge.childId));
|
|
805
|
+
const nonIncidentEdges = edges.filter((edge) => !focusIds.has(edge.parentId) && !focusIds.has(edge.childId));
|
|
806
|
+
const currentCandidates = new Map([
|
|
807
|
+
[leftId, leftNode.x],
|
|
808
|
+
[rightId, rightNode.x],
|
|
809
|
+
]);
|
|
810
|
+
const currentAffectedCrossings = yield* crossingCountForCandidateSet(context, currentCandidates);
|
|
811
|
+
const currentIncidentHorizontalSpan = incidentEdges.reduce((total, edge) => total + connectorSpans(edge).horizontal, 0);
|
|
812
|
+
const maximumNonIncidentHorizontalSpan = nonIncidentEdges.reduce((maximum, edge) => Math.max(maximum, connectorSpans(edge).horizontal), 0);
|
|
813
|
+
const otherXCoordinates = nodes.filter(({ id }) => !focusIds.has(id)).map(({ x }) => x);
|
|
814
|
+
const minimumOtherX = Math.min(...otherXCoordinates);
|
|
815
|
+
const maximumOtherX = Math.max(...otherXCoordinates);
|
|
816
|
+
for (let leftDistance = 1; leftDistance <= maximumPairDistance && !accepted; leftDistance += 1) {
|
|
817
|
+
for (const leftX of [leftNode.x - leftDistance, leftNode.x + leftDistance]) {
|
|
818
|
+
for (let rightDistance = 1; rightDistance <= maximumPairDistance && !accepted; rightDistance += 1) {
|
|
819
|
+
for (const rightX of [rightNode.x - rightDistance, rightNode.x + rightDistance]) {
|
|
820
|
+
if (context.work.consumed - repairStartedAt >= maximumRepairWork)
|
|
821
|
+
return;
|
|
822
|
+
pairCandidates += 1;
|
|
823
|
+
yield* cancellationCheckpoint(context.signal, pairCandidates, 128);
|
|
824
|
+
context.work.spend('paired global crossing repair candidate');
|
|
825
|
+
if (pairCandidates > maximumPairCandidates)
|
|
826
|
+
return;
|
|
827
|
+
const candidates = new Map([
|
|
828
|
+
[leftId, leftX],
|
|
829
|
+
[rightId, rightX],
|
|
830
|
+
]);
|
|
831
|
+
if (candidateSetConflicts(context, candidates))
|
|
832
|
+
continue;
|
|
833
|
+
const candidateAffectedCrossings = yield* crossingCountForCandidateSet(context, candidates, currentAffectedCrossings + 1);
|
|
834
|
+
if (candidateAffectedCrossings > currentAffectedCrossings)
|
|
835
|
+
continue;
|
|
836
|
+
const candidateIncidentSpans = incidentEdges.map((edge) => Math.abs((candidates.get(edge.parentId) ?? edge.parent.x) -
|
|
837
|
+
(candidates.get(edge.childId) ?? edge.child.x)));
|
|
838
|
+
const candidateMinimumX = Math.min(minimumOtherX, leftX, rightX);
|
|
839
|
+
const candidateMaximumX = Math.max(maximumOtherX, leftX, rightX);
|
|
840
|
+
const candidateScore = {
|
|
841
|
+
crossingCount: crossingSummary.count - currentAffectedCrossings + candidateAffectedCrossings,
|
|
842
|
+
maximumHorizontalSpan: Math.max(maximumNonIncidentHorizontalSpan, ...candidateIncidentSpans),
|
|
843
|
+
columnCount: candidateMaximumX - candidateMinimumX + 1,
|
|
844
|
+
totalHorizontalSpan: totalHorizontalSpan -
|
|
845
|
+
currentIncidentHorizontalSpan +
|
|
846
|
+
candidateIncidentSpans.reduce((total, span) => total + span, 0),
|
|
847
|
+
boundingCenterOffsetTwice: Math.abs(candidateMinimumX + candidateMaximumX),
|
|
848
|
+
};
|
|
849
|
+
if (!betterGlobalLayoutQualityScore(candidateScore, currentScore))
|
|
850
|
+
continue;
|
|
851
|
+
context.decisions.push({
|
|
852
|
+
focusId: leftId,
|
|
853
|
+
kind: 'moved_to_reduce_crossings',
|
|
854
|
+
message: `Paired crossing repair moved (${leftNode.x}, ${leftNode.y}) to (${leftX}, ${leftNode.y}); crossings ${currentScore.crossingCount} -> ${candidateScore.crossingCount}`,
|
|
855
|
+
}, {
|
|
856
|
+
focusId: rightId,
|
|
857
|
+
kind: 'moved_to_reduce_crossings',
|
|
858
|
+
message: `Paired crossing repair moved (${rightNode.x}, ${rightNode.y}) to (${rightX}, ${rightNode.y}); crossings ${currentScore.crossingCount} -> ${candidateScore.crossingCount}`,
|
|
859
|
+
});
|
|
860
|
+
leftNode.x = leftX;
|
|
861
|
+
rightNode.x = rightX;
|
|
862
|
+
rebuildOccupancy(context);
|
|
863
|
+
accepted = true;
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (!accepted)
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function centerFullyAutomaticLayout(context) {
|
|
876
|
+
if (context.previous.size > 0 ||
|
|
877
|
+
[...context.focuses.values()].some(({ position }) => position.mode !== 'auto') ||
|
|
878
|
+
[...context.laneBounds.values()].some(({ minimumX, maximumX }) => Number.isFinite(minimumX) || Number.isFinite(maximumX)))
|
|
879
|
+
return;
|
|
880
|
+
const nodes = [...context.placed.values()];
|
|
881
|
+
if (nodes.length === 0)
|
|
882
|
+
return;
|
|
883
|
+
const minimumX = Math.min(...nodes.map(({ x }) => x));
|
|
884
|
+
const maximumX = Math.max(...nodes.map(({ x }) => x));
|
|
885
|
+
const shift = Math.round(-(minimumX + maximumX) / 2);
|
|
886
|
+
if (shift === 0)
|
|
887
|
+
return;
|
|
888
|
+
for (const node of nodes)
|
|
889
|
+
node.x += shift;
|
|
890
|
+
for (const [laneId, base] of context.laneBases)
|
|
891
|
+
context.laneBases.set(laneId, base + shift);
|
|
892
|
+
rebuildOccupancy(context);
|
|
893
|
+
}
|
|
368
894
|
function* placeFocus(context, focusId, inheritedStack = new Set()) {
|
|
369
895
|
yield* cancellationCheckpoint(context.signal);
|
|
370
896
|
context.work.spend('focus placement');
|
|
@@ -468,52 +994,65 @@ function* placeFocus(context, focusId, inheritedStack = new Set()) {
|
|
|
468
994
|
context.decisions.push({
|
|
469
995
|
focusId,
|
|
470
996
|
kind: 'preserved',
|
|
471
|
-
message: `Preserved prior automatic coordinate (${node.x}, ${node.y})
|
|
997
|
+
message: `Preserved prior automatic coordinate (${node.x}, ${node.y})`,
|
|
472
998
|
});
|
|
999
|
+
recordPlaced(context, node);
|
|
1000
|
+
return node;
|
|
473
1001
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
1002
|
+
const structuralX = structuralAnchorX(context, focus, parents, laneId) +
|
|
1003
|
+
(focus.position.preferredX === undefined ? (context.siblingOffsets.get(focusId) ?? 0) : 0);
|
|
1004
|
+
const requestedPreferredX = focus.position.preferredX ?? structuralX;
|
|
1005
|
+
const requestedPreferredY = focus.position.preferredY ?? requiredY;
|
|
1006
|
+
const bounds = boundsForFocus(context, focusId);
|
|
1007
|
+
const preferredX = Math.max(bounds.minimumX, Math.min(bounds.maximumX, requestedPreferredX));
|
|
1008
|
+
const preferredY = Math.max(requiredY, requestedPreferredY);
|
|
1009
|
+
const coordinate = yield* availableX(context, focusId, preferredX, preferredY);
|
|
1010
|
+
const moved = coordinate.x !== requestedPreferredX || preferredY !== requestedPreferredY;
|
|
1011
|
+
node = {
|
|
1012
|
+
id: focusId,
|
|
1013
|
+
x: coordinate.x,
|
|
1014
|
+
y: preferredY,
|
|
1015
|
+
laneId,
|
|
1016
|
+
preserved: false,
|
|
1017
|
+
sourceMode: 'auto',
|
|
1018
|
+
};
|
|
1019
|
+
const explanations = [];
|
|
1020
|
+
if (preferredX !== requestedPreferredX) {
|
|
1021
|
+
explanations.push(`lane ${laneId} bounds clamped x ${requestedPreferredX} -> ${preferredX}`);
|
|
1022
|
+
}
|
|
1023
|
+
if (preferredY !== requestedPreferredY) {
|
|
1024
|
+
explanations.push(`parent ordering raised y ${requestedPreferredY} -> ${preferredY}`);
|
|
1025
|
+
}
|
|
1026
|
+
if (coordinate.conflictsAtPreferred.visibleOverlaps.length > 0) {
|
|
1027
|
+
explanations.push(`visible overlap with ${coordinate.conflictsAtPreferred.visibleOverlaps.join(', ')}`);
|
|
1028
|
+
}
|
|
1029
|
+
if (coordinate.conflictsAtPreferred.focusSpacing.length > 0) {
|
|
1030
|
+
explanations.push(`same-row spacing from ${coordinate.conflictsAtPreferred.focusSpacing.join(', ')}`);
|
|
1031
|
+
}
|
|
1032
|
+
if (coordinate.conflictsAtPreferred.mutualExclusions.length > 0) {
|
|
1033
|
+
explanations.push(`mutual-exclusion spacing from ${coordinate.conflictsAtPreferred.mutualExclusions.join(', ')}`);
|
|
1034
|
+
}
|
|
1035
|
+
if (coordinate.crossings < coordinate.crossingsAtPreferred) {
|
|
1036
|
+
explanations.push(`connector crossings ${coordinate.crossingsAtPreferred} -> ${coordinate.crossings}`);
|
|
1037
|
+
}
|
|
1038
|
+
const movedKind = coordinate.conflictsAtPreferred.visibleOverlaps.length > 0
|
|
1039
|
+
? 'moved_for_collision'
|
|
1040
|
+
: coordinate.conflictsAtPreferred.focusSpacing.length > 0
|
|
1041
|
+
? 'moved_for_spacing'
|
|
506
1042
|
: coordinate.conflictsAtPreferred.mutualExclusions.length > 0
|
|
507
1043
|
? 'moved_for_mutual_exclusion'
|
|
508
|
-
:
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
1044
|
+
: preferredY !== requestedPreferredY
|
|
1045
|
+
? 'moved_for_parent_order'
|
|
1046
|
+
: preferredX !== requestedPreferredX
|
|
1047
|
+
? 'moved_for_lane_bounds'
|
|
1048
|
+
: 'moved_to_reduce_crossings';
|
|
1049
|
+
context.decisions.push({
|
|
1050
|
+
focusId,
|
|
1051
|
+
kind: moved ? movedKind : 'placed',
|
|
1052
|
+
message: moved
|
|
1053
|
+
? `Moved from preferred (${requestedPreferredX}, ${requestedPreferredY}) to (${node.x}, ${node.y}) to resolve ${explanations.join(' and ')}`
|
|
1054
|
+
: `Placed in lane ${laneId} at (${node.x}, ${node.y}); connector crossings ${coordinate.crossings}`,
|
|
1055
|
+
});
|
|
517
1056
|
}
|
|
518
1057
|
const cycleFallback = context.placed.get(focusId);
|
|
519
1058
|
if (cycleFallback !== undefined)
|
|
@@ -549,6 +1088,56 @@ function* visibleOverlapDiagnostics(context) {
|
|
|
549
1088
|
}
|
|
550
1089
|
}
|
|
551
1090
|
}
|
|
1091
|
+
function* sameRowSpacingDiagnostics(context) {
|
|
1092
|
+
const rows = new Map();
|
|
1093
|
+
for (const node of context.placed.values()) {
|
|
1094
|
+
const row = rows.get(node.y) ?? [];
|
|
1095
|
+
row.push(node);
|
|
1096
|
+
rows.set(node.y, row);
|
|
1097
|
+
}
|
|
1098
|
+
let sameRowPairCount = 0;
|
|
1099
|
+
let tooCloseSameRowPairCount = 0;
|
|
1100
|
+
let minimumSameRowSpacing = Infinity;
|
|
1101
|
+
let rowIndex = 0;
|
|
1102
|
+
for (const [y, row] of [...rows].sort((left, right) => left[0] - right[0])) {
|
|
1103
|
+
yield* cancellationCheckpoint(context.signal, rowIndex, 16);
|
|
1104
|
+
rowIndex += 1;
|
|
1105
|
+
row.sort((left, right) => left.x - right.x || compareCodeUnits(left.id, right.id));
|
|
1106
|
+
for (let index = 1; index < row.length; index += 1) {
|
|
1107
|
+
context.work.spend('same-row spacing diagnostic');
|
|
1108
|
+
const left = row[index - 1];
|
|
1109
|
+
const right = row[index];
|
|
1110
|
+
if (left === undefined || right === undefined)
|
|
1111
|
+
continue;
|
|
1112
|
+
const spacing = right.x - left.x;
|
|
1113
|
+
sameRowPairCount += 1;
|
|
1114
|
+
minimumSameRowSpacing = Math.min(minimumSameRowSpacing, spacing);
|
|
1115
|
+
if (spacing >= context.nodeSpacing)
|
|
1116
|
+
continue;
|
|
1117
|
+
tooCloseSameRowPairCount += 1;
|
|
1118
|
+
const focus = context.focuses.get(right.id);
|
|
1119
|
+
context.diagnostics.push({
|
|
1120
|
+
code: 'FOCUS_LAYOUT_SAME_ROW_SPACING_UNSATISFIED',
|
|
1121
|
+
severity: 'warning',
|
|
1122
|
+
category: 'layout',
|
|
1123
|
+
message: `Focuses ${left.id} and ${right.id} are ${spacing} columns apart on row ${y}; ${context.nodeSpacing} are required`,
|
|
1124
|
+
...(focus?.sourceLocation === undefined ? {} : { location: focus.sourceLocation }),
|
|
1125
|
+
details: {
|
|
1126
|
+
focusIds: [left.id, right.id],
|
|
1127
|
+
y,
|
|
1128
|
+
actualSpacing: spacing,
|
|
1129
|
+
requiredSpacing: context.nodeSpacing,
|
|
1130
|
+
movableFocusIds: [left.id, right.id].filter((id) => canMoveForSoftConstraint(context, id)),
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
sameRowPairCount,
|
|
1137
|
+
tooCloseSameRowPairCount,
|
|
1138
|
+
minimumSameRowSpacing: Number.isFinite(minimumSameRowSpacing) ? minimumSameRowSpacing : 0,
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
552
1141
|
function canMoveForSoftConstraint(context, focusId) {
|
|
553
1142
|
const focus = context.focuses.get(focusId);
|
|
554
1143
|
return focus?.position.mode === 'auto' && !context.previous.has(focusId);
|
|
@@ -581,7 +1170,7 @@ function* mutualExclusionSpacingDiagnostics(context) {
|
|
|
581
1170
|
const preservedFocusIds = [leftId, rightId].filter((id) => !canMoveForSoftConstraint(context, id));
|
|
582
1171
|
const focus = context.focuses.get(rightId);
|
|
583
1172
|
const reason = movableFocusIds.length === 0
|
|
584
|
-
? 'both endpoints are fixed
|
|
1173
|
+
? 'both endpoints are fixed or relative'
|
|
585
1174
|
: 'the remaining movable endpoint cannot change the authored relative relationship safely';
|
|
586
1175
|
context.diagnostics.push({
|
|
587
1176
|
code: 'FOCUS_LAYOUT_MUTUAL_EXCLUSION_SPACING_UNSATISFIED',
|
|
@@ -599,17 +1188,47 @@ function* mutualExclusionSpacingDiagnostics(context) {
|
|
|
599
1188
|
});
|
|
600
1189
|
}
|
|
601
1190
|
}
|
|
602
|
-
function*
|
|
1191
|
+
function* connectorNodeIntersectionDiagnostics(context, edges) {
|
|
1192
|
+
const nodes = [...context.placed.values()].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
1193
|
+
let comparisonIndex = 0;
|
|
1194
|
+
let intersectionCount = 0;
|
|
1195
|
+
for (const edge of edges) {
|
|
1196
|
+
const flattened = flattenFocusConnectorCurve(focusConnectorCurve(edge.parent, edge.child));
|
|
1197
|
+
for (const node of nodes) {
|
|
1198
|
+
yield* cancellationCheckpoint(context.signal, comparisonIndex, 32);
|
|
1199
|
+
comparisonIndex += 1;
|
|
1200
|
+
if (node.id === edge.parentId || node.id === edge.childId)
|
|
1201
|
+
continue;
|
|
1202
|
+
context.work.spend('connector-node intersection comparison');
|
|
1203
|
+
if (!focusConnectorPolylineIntersectsRectangle(flattened, focusNodeRectangle(node)))
|
|
1204
|
+
continue;
|
|
1205
|
+
intersectionCount += 1;
|
|
1206
|
+
const focus = context.focuses.get(node.id);
|
|
1207
|
+
context.diagnostics.push({
|
|
1208
|
+
code: 'FOCUS_LAYOUT_CONNECTOR_THROUGH_NODE',
|
|
1209
|
+
severity: 'warning',
|
|
1210
|
+
category: 'layout',
|
|
1211
|
+
message: `Connector ${edge.parentId} -> ${edge.childId} intersects unrelated focus ${node.id}`,
|
|
1212
|
+
...(focus?.sourceLocation === undefined ? {} : { location: focus.sourceLocation }),
|
|
1213
|
+
details: {
|
|
1214
|
+
parentId: edge.parentId,
|
|
1215
|
+
childId: edge.childId,
|
|
1216
|
+
focusId: node.id,
|
|
1217
|
+
},
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return intersectionCount;
|
|
1222
|
+
}
|
|
1223
|
+
function* connectorCrossingDiagnostics(context, crossings) {
|
|
603
1224
|
yield* cancellationCheckpoint(context.signal);
|
|
604
|
-
const edges = yield* connectorEdges(context);
|
|
605
|
-
const crossings = yield* connectorCrossings(edges, context);
|
|
606
1225
|
for (const [first, second] of crossings) {
|
|
607
1226
|
const endpointIds = [first.parentId, first.childId, second.parentId, second.childId].filter((id, index, all) => all.indexOf(id) === index);
|
|
608
1227
|
const movableFocusIds = endpointIds.filter((id) => canMoveForSoftConstraint(context, id));
|
|
609
1228
|
const preservedFocusIds = endpointIds.filter((id) => !canMoveForSoftConstraint(context, id));
|
|
610
1229
|
const focus = context.focuses.get(second.childId);
|
|
611
1230
|
const reason = movableFocusIds.length === 0
|
|
612
|
-
? 'all connector endpoints are fixed
|
|
1231
|
+
? 'all connector endpoints are fixed or relative'
|
|
613
1232
|
: 'no collision-free deterministic candidate reduced the crossing without moving preserved endpoints';
|
|
614
1233
|
context.diagnostics.push({
|
|
615
1234
|
code: 'FOCUS_LAYOUT_CONNECTOR_CROSSING_UNSATISFIED',
|
|
@@ -626,6 +1245,536 @@ function* connectorCrossingDiagnostics(context) {
|
|
|
626
1245
|
});
|
|
627
1246
|
}
|
|
628
1247
|
}
|
|
1248
|
+
function connectorSpans(edge) {
|
|
1249
|
+
const horizontal = Math.abs(edge.parent.x - edge.child.x);
|
|
1250
|
+
const vertical = Math.abs(edge.parent.y - edge.child.y);
|
|
1251
|
+
return { horizontal, vertical, manhattan: horizontal + vertical };
|
|
1252
|
+
}
|
|
1253
|
+
function longConnectorSpans(spans) {
|
|
1254
|
+
return (spans.horizontal > LONG_CONNECTOR_HORIZONTAL_SPAN ||
|
|
1255
|
+
spans.vertical > LONG_CONNECTOR_VERTICAL_SPAN ||
|
|
1256
|
+
spans.manhattan > LONG_CONNECTOR_MANHATTAN_SPAN);
|
|
1257
|
+
}
|
|
1258
|
+
function longConnector(edge) {
|
|
1259
|
+
return longConnectorSpans(connectorSpans(edge));
|
|
1260
|
+
}
|
|
1261
|
+
function betterConnectorQualityScore(candidate, current) {
|
|
1262
|
+
if (candidate.longConnectorCount > current.longConnectorCount)
|
|
1263
|
+
return false;
|
|
1264
|
+
const fields = [
|
|
1265
|
+
'crossingCount',
|
|
1266
|
+
'nodeIntersectionCount',
|
|
1267
|
+
'maximumManhattanSpan',
|
|
1268
|
+
'maximumHorizontalSpan',
|
|
1269
|
+
'longConnectorCount',
|
|
1270
|
+
'totalManhattanSpan',
|
|
1271
|
+
'totalHorizontalSpan',
|
|
1272
|
+
'asymmetricSiblingCohortCount',
|
|
1273
|
+
'maximumSiblingDeviation',
|
|
1274
|
+
'totalSiblingDeviation',
|
|
1275
|
+
'offAnchorSiblingCohortCount',
|
|
1276
|
+
'maximumSiblingAnchorDeviation',
|
|
1277
|
+
'totalSiblingAnchorDeviation',
|
|
1278
|
+
'columnCount',
|
|
1279
|
+
'boundingCenterOffsetTwice',
|
|
1280
|
+
];
|
|
1281
|
+
for (const field of fields) {
|
|
1282
|
+
if (candidate[field] === current[field])
|
|
1283
|
+
continue;
|
|
1284
|
+
return candidate[field] < current[field];
|
|
1285
|
+
}
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
function candidateNode(context, focusId, x) {
|
|
1289
|
+
const current = context.placed.get(focusId);
|
|
1290
|
+
return current === undefined ? undefined : { ...current, x };
|
|
1291
|
+
}
|
|
1292
|
+
function* affectedNodeIntersectionCount(context, edges, focusId, x) {
|
|
1293
|
+
const moved = candidateNode(context, focusId, x);
|
|
1294
|
+
if (moved === undefined)
|
|
1295
|
+
return 0;
|
|
1296
|
+
const nodes = [...context.placed.values()];
|
|
1297
|
+
let count = 0;
|
|
1298
|
+
let comparisonIndex = 0;
|
|
1299
|
+
for (const definition of context.connectorDefinitionsByFocus.get(focusId) ?? []) {
|
|
1300
|
+
const edge = candidateConnector(context, definition, new Map([[focusId, x]]));
|
|
1301
|
+
if (edge === undefined)
|
|
1302
|
+
continue;
|
|
1303
|
+
const flattened = flattenFocusConnectorCurve(focusConnectorCurve(edge.parent, edge.child));
|
|
1304
|
+
for (const node of nodes) {
|
|
1305
|
+
yield* cancellationCheckpoint(context.signal, comparisonIndex, 64);
|
|
1306
|
+
comparisonIndex += 1;
|
|
1307
|
+
if (node.id === edge.parentId || node.id === edge.childId)
|
|
1308
|
+
continue;
|
|
1309
|
+
context.work.spend('quality candidate connector-node comparison');
|
|
1310
|
+
if (focusConnectorPolylineIntersectsRectangle(flattened, focusNodeRectangle(node)))
|
|
1311
|
+
count += 1;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
const rectangle = focusNodeRectangle(moved);
|
|
1315
|
+
for (const edge of edges) {
|
|
1316
|
+
if (edge.parentId === focusId || edge.childId === focusId)
|
|
1317
|
+
continue;
|
|
1318
|
+
yield* cancellationCheckpoint(context.signal, comparisonIndex, 64);
|
|
1319
|
+
comparisonIndex += 1;
|
|
1320
|
+
context.work.spend('quality candidate node-connector comparison');
|
|
1321
|
+
const flattened = flattenFocusConnectorCurve(focusConnectorCurve(edge.parent, edge.child));
|
|
1322
|
+
if (focusConnectorPolylineIntersectsRectangle(flattened, rectangle))
|
|
1323
|
+
count += 1;
|
|
1324
|
+
}
|
|
1325
|
+
return count;
|
|
1326
|
+
}
|
|
1327
|
+
function* connectorNodeIntersectionSummary(context, edges) {
|
|
1328
|
+
const nodes = [...context.placed.values()].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
1329
|
+
const involvedFocusIds = new Set();
|
|
1330
|
+
let count = 0;
|
|
1331
|
+
let comparisonIndex = 0;
|
|
1332
|
+
for (const edge of edges) {
|
|
1333
|
+
const flattened = flattenFocusConnectorCurve(focusConnectorCurve(edge.parent, edge.child));
|
|
1334
|
+
for (const node of nodes) {
|
|
1335
|
+
yield* cancellationCheckpoint(context.signal, comparisonIndex, 64);
|
|
1336
|
+
comparisonIndex += 1;
|
|
1337
|
+
if (node.id === edge.parentId || node.id === edge.childId)
|
|
1338
|
+
continue;
|
|
1339
|
+
context.work.spend('quality connector-node comparison');
|
|
1340
|
+
if (!focusConnectorPolylineIntersectsRectangle(flattened, focusNodeRectangle(node)))
|
|
1341
|
+
continue;
|
|
1342
|
+
count += 1;
|
|
1343
|
+
involvedFocusIds.add(edge.parentId);
|
|
1344
|
+
involvedFocusIds.add(edge.childId);
|
|
1345
|
+
involvedFocusIds.add(node.id);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return { count, involvedFocusIds };
|
|
1349
|
+
}
|
|
1350
|
+
function siblingQuality(context, override) {
|
|
1351
|
+
let asymmetricSiblingCohortCount = 0;
|
|
1352
|
+
let maximumSiblingDeviation = 0;
|
|
1353
|
+
let totalSiblingDeviation = 0;
|
|
1354
|
+
let offAnchorSiblingCohortCount = 0;
|
|
1355
|
+
let maximumSiblingAnchorDeviation = 0;
|
|
1356
|
+
let totalSiblingAnchorDeviation = 0;
|
|
1357
|
+
const parentFocusIds = new Set();
|
|
1358
|
+
for (const cohort of context.siblingCohorts) {
|
|
1359
|
+
const focus = context.focuses.get(cohort[0] ?? '');
|
|
1360
|
+
if (focus === undefined)
|
|
1361
|
+
continue;
|
|
1362
|
+
const coordinates = cohort
|
|
1363
|
+
.flatMap((focusId) => {
|
|
1364
|
+
const node = context.placed.get(focusId);
|
|
1365
|
+
return node === undefined ? [] : [override?.get(focusId) ?? node.x];
|
|
1366
|
+
})
|
|
1367
|
+
.sort((left, right) => left - right);
|
|
1368
|
+
const parentIds = prerequisiteIds(focus);
|
|
1369
|
+
for (const parentId of parentIds)
|
|
1370
|
+
parentFocusIds.add(parentId);
|
|
1371
|
+
const parentCoordinates = parentIds
|
|
1372
|
+
.flatMap((parentId) => {
|
|
1373
|
+
const node = context.placed.get(parentId);
|
|
1374
|
+
if (node === undefined)
|
|
1375
|
+
return [];
|
|
1376
|
+
return [override?.get(parentId) ?? node.x];
|
|
1377
|
+
})
|
|
1378
|
+
.sort((left, right) => left - right);
|
|
1379
|
+
let anchorX;
|
|
1380
|
+
if (coordinates.length >= 4 &&
|
|
1381
|
+
(coordinates.at(-1) ?? 0) - (coordinates[0] ?? 0) > 2 * LONG_CONNECTOR_HORIZONTAL_SPAN) {
|
|
1382
|
+
anchorX = ((coordinates[0] ?? 0) + (coordinates.at(-1) ?? 0)) / 2;
|
|
1383
|
+
}
|
|
1384
|
+
else if (parentCoordinates.length > 1 && focus.convergence) {
|
|
1385
|
+
anchorX = Math.floor(parentCoordinates.reduce((total, coordinate) => total + coordinate, 0) /
|
|
1386
|
+
parentCoordinates.length);
|
|
1387
|
+
}
|
|
1388
|
+
else if (parentCoordinates.length > 0) {
|
|
1389
|
+
const middle = Math.floor(parentCoordinates.length / 2);
|
|
1390
|
+
anchorX =
|
|
1391
|
+
parentCoordinates.length % 2 === 1
|
|
1392
|
+
? (parentCoordinates[middle] ?? 0)
|
|
1393
|
+
: Math.floor(((parentCoordinates[middle - 1] ?? 0) + (parentCoordinates[middle] ?? 0)) / 2);
|
|
1394
|
+
}
|
|
1395
|
+
else {
|
|
1396
|
+
anchorX = context.laneBases.get(context.laneIds.get(focus.id) ?? 'default') ?? 0;
|
|
1397
|
+
}
|
|
1398
|
+
const internalCenterTwice = (coordinates[0] ?? 0) + (coordinates.at(-1) ?? 0);
|
|
1399
|
+
let internalDeviation = 0;
|
|
1400
|
+
let anchorDeviation = 0;
|
|
1401
|
+
for (let index = 0; index <= Math.floor((coordinates.length - 1) / 2); index += 1) {
|
|
1402
|
+
const pairSum = (coordinates[index] ?? 0) + (coordinates[coordinates.length - 1 - index] ?? 0);
|
|
1403
|
+
internalDeviation += Math.abs(pairSum - internalCenterTwice);
|
|
1404
|
+
anchorDeviation += Math.abs(pairSum - 2 * anchorX);
|
|
1405
|
+
}
|
|
1406
|
+
totalSiblingDeviation += internalDeviation;
|
|
1407
|
+
maximumSiblingDeviation = Math.max(maximumSiblingDeviation, internalDeviation);
|
|
1408
|
+
if (internalDeviation > 0)
|
|
1409
|
+
asymmetricSiblingCohortCount += 1;
|
|
1410
|
+
totalSiblingAnchorDeviation += anchorDeviation;
|
|
1411
|
+
maximumSiblingAnchorDeviation = Math.max(maximumSiblingAnchorDeviation, anchorDeviation);
|
|
1412
|
+
if (anchorDeviation > 0)
|
|
1413
|
+
offAnchorSiblingCohortCount += 1;
|
|
1414
|
+
}
|
|
1415
|
+
return {
|
|
1416
|
+
asymmetricSiblingCohortCount,
|
|
1417
|
+
maximumSiblingDeviation,
|
|
1418
|
+
totalSiblingDeviation,
|
|
1419
|
+
offAnchorSiblingCohortCount,
|
|
1420
|
+
maximumSiblingAnchorDeviation,
|
|
1421
|
+
totalSiblingAnchorDeviation,
|
|
1422
|
+
parentFocusIds,
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
function connectorQualityScore(context, edges, crossingCount, nodeIntersectionCount, symmetry) {
|
|
1426
|
+
const spans = edges.map(connectorSpans);
|
|
1427
|
+
const nodes = [...context.placed.values()];
|
|
1428
|
+
const minimumX = Math.min(...nodes.map(({ x }) => x));
|
|
1429
|
+
const maximumX = Math.max(...nodes.map(({ x }) => x));
|
|
1430
|
+
return {
|
|
1431
|
+
crossingCount,
|
|
1432
|
+
nodeIntersectionCount,
|
|
1433
|
+
asymmetricSiblingCohortCount: symmetry.asymmetricSiblingCohortCount,
|
|
1434
|
+
maximumSiblingDeviation: symmetry.maximumSiblingDeviation,
|
|
1435
|
+
totalSiblingDeviation: symmetry.totalSiblingDeviation,
|
|
1436
|
+
offAnchorSiblingCohortCount: symmetry.offAnchorSiblingCohortCount,
|
|
1437
|
+
maximumSiblingAnchorDeviation: symmetry.maximumSiblingAnchorDeviation,
|
|
1438
|
+
totalSiblingAnchorDeviation: symmetry.totalSiblingAnchorDeviation,
|
|
1439
|
+
longConnectorCount: edges.filter(longConnector).length,
|
|
1440
|
+
maximumManhattanSpan: spans.reduce((maximum, { manhattan }) => Math.max(maximum, manhattan), 0),
|
|
1441
|
+
maximumHorizontalSpan: spans.reduce((maximum, { horizontal }) => Math.max(maximum, horizontal), 0),
|
|
1442
|
+
totalManhattanSpan: spans.reduce((total, { manhattan }) => total + manhattan, 0),
|
|
1443
|
+
totalHorizontalSpan: spans.reduce((total, { horizontal }) => total + horizontal, 0),
|
|
1444
|
+
columnCount: maximumX - minimumX + 1,
|
|
1445
|
+
boundingCenterOffsetTwice: Math.abs(minimumX + maximumX),
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
function candidateQualityScore(context, edges, current, focusId, x, currentAffectedCrossings, candidateAffectedCrossings, currentAffectedNodeIntersections, candidateAffectedNodeIntersections) {
|
|
1449
|
+
const incidentEdges = edges.filter((edge) => edge.parentId === focusId || edge.childId === focusId);
|
|
1450
|
+
const nonIncidentEdges = edges.filter((edge) => edge.parentId !== focusId && edge.childId !== focusId);
|
|
1451
|
+
const candidateIncidentEdges = incidentEdges.map((edge) => ({
|
|
1452
|
+
...edge,
|
|
1453
|
+
parent: edge.parentId === focusId ? { ...edge.parent, x } : edge.parent,
|
|
1454
|
+
child: edge.childId === focusId ? { ...edge.child, x } : edge.child,
|
|
1455
|
+
}));
|
|
1456
|
+
const currentIncidentSpans = incidentEdges.map(connectorSpans);
|
|
1457
|
+
const candidateIncidentSpans = candidateIncidentEdges.map(connectorSpans);
|
|
1458
|
+
const nonIncidentSpans = nonIncidentEdges.map(connectorSpans);
|
|
1459
|
+
const otherXCoordinates = [...context.placed.values()]
|
|
1460
|
+
.filter(({ id }) => id !== focusId)
|
|
1461
|
+
.map(({ x: coordinate }) => coordinate);
|
|
1462
|
+
const minimumX = Math.min(x, ...otherXCoordinates);
|
|
1463
|
+
const maximumX = Math.max(x, ...otherXCoordinates);
|
|
1464
|
+
const symmetry = siblingQuality(context, new Map([[focusId, x]]));
|
|
1465
|
+
const currentLong = currentIncidentSpans.filter(longConnectorSpans).length;
|
|
1466
|
+
const candidateLong = candidateIncidentEdges.filter(longConnector).length;
|
|
1467
|
+
const totalHorizontal = (values) => values.reduce((total, { horizontal }) => total + horizontal, 0);
|
|
1468
|
+
const totalManhattan = (values) => values.reduce((total, { manhattan }) => total + manhattan, 0);
|
|
1469
|
+
return {
|
|
1470
|
+
crossingCount: current.crossingCount - currentAffectedCrossings + candidateAffectedCrossings,
|
|
1471
|
+
nodeIntersectionCount: current.nodeIntersectionCount -
|
|
1472
|
+
currentAffectedNodeIntersections +
|
|
1473
|
+
candidateAffectedNodeIntersections,
|
|
1474
|
+
asymmetricSiblingCohortCount: symmetry.asymmetricSiblingCohortCount,
|
|
1475
|
+
maximumSiblingDeviation: symmetry.maximumSiblingDeviation,
|
|
1476
|
+
totalSiblingDeviation: symmetry.totalSiblingDeviation,
|
|
1477
|
+
offAnchorSiblingCohortCount: symmetry.offAnchorSiblingCohortCount,
|
|
1478
|
+
maximumSiblingAnchorDeviation: symmetry.maximumSiblingAnchorDeviation,
|
|
1479
|
+
totalSiblingAnchorDeviation: symmetry.totalSiblingAnchorDeviation,
|
|
1480
|
+
longConnectorCount: current.longConnectorCount - currentLong + candidateLong,
|
|
1481
|
+
maximumManhattanSpan: Math.max(0, ...nonIncidentSpans.map(({ manhattan }) => manhattan), ...candidateIncidentSpans.map(({ manhattan }) => manhattan)),
|
|
1482
|
+
maximumHorizontalSpan: Math.max(0, ...nonIncidentSpans.map(({ horizontal }) => horizontal), ...candidateIncidentSpans.map(({ horizontal }) => horizontal)),
|
|
1483
|
+
totalManhattanSpan: current.totalManhattanSpan -
|
|
1484
|
+
totalManhattan(currentIncidentSpans) +
|
|
1485
|
+
totalManhattan(candidateIncidentSpans),
|
|
1486
|
+
totalHorizontalSpan: current.totalHorizontalSpan -
|
|
1487
|
+
totalHorizontal(currentIncidentSpans) +
|
|
1488
|
+
totalHorizontal(candidateIncidentSpans),
|
|
1489
|
+
columnCount: maximumX - minimumX + 1,
|
|
1490
|
+
boundingCenterOffsetTwice: Math.abs(minimumX + maximumX),
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
const CONNECTOR_REFINEMENT_WORK_MAX = 7_000_000;
|
|
1494
|
+
function* refineConnectorQualityWithinBudget(context) {
|
|
1495
|
+
const maximumAcceptedMoves = 24;
|
|
1496
|
+
const maximumCandidateNodes = 32;
|
|
1497
|
+
const edges = yield* connectorEdges(context);
|
|
1498
|
+
const crossingSummary = yield* connectorCrossings(edges, context);
|
|
1499
|
+
const intersectionSummary = yield* connectorNodeIntersectionSummary(context, edges);
|
|
1500
|
+
const symmetry = siblingQuality(context);
|
|
1501
|
+
let current = connectorQualityScore(context, edges, crossingSummary.count, intersectionSummary.count, symmetry);
|
|
1502
|
+
const siblingMembers = new Set(context.siblingCohorts.flat());
|
|
1503
|
+
const candidateIds = new Set(intersectionSummary.involvedFocusIds);
|
|
1504
|
+
for (const edge of edges.filter(longConnector)) {
|
|
1505
|
+
candidateIds.add(edge.parentId);
|
|
1506
|
+
candidateIds.add(edge.childId);
|
|
1507
|
+
}
|
|
1508
|
+
for (const parentId of symmetry.parentFocusIds)
|
|
1509
|
+
candidateIds.add(parentId);
|
|
1510
|
+
const orderedCandidateIds = [...candidateIds]
|
|
1511
|
+
.filter((focusId) => {
|
|
1512
|
+
const degree = context.connectorDefinitionsByFocus.get(focusId)?.length ?? 0;
|
|
1513
|
+
return (canMoveForSoftConstraint(context, focusId) && !siblingMembers.has(focusId) && degree !== 1);
|
|
1514
|
+
})
|
|
1515
|
+
.sort((left, right) => {
|
|
1516
|
+
const degree = (focusId) => context.connectorDefinitionsByFocus.get(focusId)?.length ?? 0;
|
|
1517
|
+
return degree(right) - degree(left) || compareCodeUnits(left, right);
|
|
1518
|
+
})
|
|
1519
|
+
.slice(0, maximumCandidateNodes);
|
|
1520
|
+
for (let acceptedMoves = 0; acceptedMoves < maximumAcceptedMoves; acceptedMoves += 1) {
|
|
1521
|
+
let best;
|
|
1522
|
+
for (const [focusIndex, focusId] of orderedCandidateIds.entries()) {
|
|
1523
|
+
yield* cancellationCheckpoint(context.signal, focusIndex, 8);
|
|
1524
|
+
const node = context.placed.get(focusId);
|
|
1525
|
+
if (node === undefined)
|
|
1526
|
+
continue;
|
|
1527
|
+
const adjacent = (context.connectorDefinitionsByFocus.get(focusId) ?? []).flatMap((definition) => {
|
|
1528
|
+
const otherId = definition.parentId === focusId ? definition.childId : definition.parentId;
|
|
1529
|
+
const other = context.placed.get(otherId);
|
|
1530
|
+
return other === undefined ? [] : [other.x];
|
|
1531
|
+
});
|
|
1532
|
+
const targets = new Set();
|
|
1533
|
+
for (let distance = 1; distance <= 4; distance += 1) {
|
|
1534
|
+
targets.add(node.x - distance);
|
|
1535
|
+
targets.add(node.x + distance);
|
|
1536
|
+
}
|
|
1537
|
+
if (adjacent.length > 0) {
|
|
1538
|
+
const midpoint = Math.round((Math.min(...adjacent) + Math.max(...adjacent)) / 2);
|
|
1539
|
+
for (let offset = -2; offset <= 2; offset += 1)
|
|
1540
|
+
targets.add(midpoint + offset);
|
|
1541
|
+
}
|
|
1542
|
+
const currentAffectedCrossings = yield* crossingCountForCandidate(context, focusId, node.x, node.y);
|
|
1543
|
+
const currentAffectedNodeIntersections = yield* affectedNodeIntersectionCount(context, edges, focusId, node.x);
|
|
1544
|
+
for (const x of [...targets].sort((left, right) => left - right)) {
|
|
1545
|
+
if (x === node.x || !coordinateWithinLane(context, focusId, x))
|
|
1546
|
+
continue;
|
|
1547
|
+
if (conflictsCandidate(candidateConflicts(context, focusId, x, node.y)))
|
|
1548
|
+
continue;
|
|
1549
|
+
const candidateAffectedCrossings = yield* crossingCountForCandidate(context, focusId, x, node.y, currentAffectedCrossings + 1);
|
|
1550
|
+
if (candidateAffectedCrossings > currentAffectedCrossings)
|
|
1551
|
+
continue;
|
|
1552
|
+
const candidateAffectedNodeIntersections = yield* affectedNodeIntersectionCount(context, edges, focusId, x);
|
|
1553
|
+
const score = candidateQualityScore(context, edges, current, focusId, x, currentAffectedCrossings, candidateAffectedCrossings, currentAffectedNodeIntersections, candidateAffectedNodeIntersections);
|
|
1554
|
+
if (!betterConnectorQualityScore(score, current))
|
|
1555
|
+
continue;
|
|
1556
|
+
if (best !== undefined && !betterConnectorQualityScore(score, best.score))
|
|
1557
|
+
continue;
|
|
1558
|
+
best = {
|
|
1559
|
+
focusId,
|
|
1560
|
+
x,
|
|
1561
|
+
score,
|
|
1562
|
+
message: `Connector-quality refinement moved (${node.x}, ${node.y}) to (${x}, ${node.y})`,
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
if (best === undefined)
|
|
1567
|
+
return;
|
|
1568
|
+
const node = context.placed.get(best.focusId);
|
|
1569
|
+
if (node === undefined)
|
|
1570
|
+
return;
|
|
1571
|
+
context.decisions.push({
|
|
1572
|
+
focusId: best.focusId,
|
|
1573
|
+
kind: 'moved_to_improve_connector_quality',
|
|
1574
|
+
message: best.message,
|
|
1575
|
+
});
|
|
1576
|
+
movePlacedFocusX(context, node, best.x);
|
|
1577
|
+
current = best.score;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
function* refineConnectorQuality(context) {
|
|
1581
|
+
const operationBudget = context.work;
|
|
1582
|
+
context.work = new FocusLayoutSubBudget(operationBudget, CONNECTOR_REFINEMENT_WORK_MAX);
|
|
1583
|
+
try {
|
|
1584
|
+
yield* refineConnectorQualityWithinBudget(context);
|
|
1585
|
+
}
|
|
1586
|
+
catch (error) {
|
|
1587
|
+
if (!(error instanceof FocusLayoutSubBudgetExhausted))
|
|
1588
|
+
throw error;
|
|
1589
|
+
}
|
|
1590
|
+
finally {
|
|
1591
|
+
context.work = operationBudget;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
function* connectorLengthDiagnostics(context, edges) {
|
|
1595
|
+
for (const [edgeIndex, edge] of edges.entries()) {
|
|
1596
|
+
yield* cancellationCheckpoint(context.signal, edgeIndex, 32);
|
|
1597
|
+
context.work.spend('connector length diagnostic');
|
|
1598
|
+
if (!longConnector(edge))
|
|
1599
|
+
continue;
|
|
1600
|
+
const spans = connectorSpans(edge);
|
|
1601
|
+
const focus = context.focuses.get(edge.childId);
|
|
1602
|
+
context.diagnostics.push({
|
|
1603
|
+
code: 'FOCUS_LAYOUT_LONG_CONNECTOR',
|
|
1604
|
+
severity: 'warning',
|
|
1605
|
+
category: 'layout',
|
|
1606
|
+
message: `Connector ${edge.parentId} -> ${edge.childId} spans ${spans.horizontal} columns and ${spans.vertical} rows`,
|
|
1607
|
+
...(focus?.sourceLocation === undefined ? {} : { location: focus.sourceLocation }),
|
|
1608
|
+
details: {
|
|
1609
|
+
parentId: edge.parentId,
|
|
1610
|
+
childId: edge.childId,
|
|
1611
|
+
horizontalSpan: spans.horizontal,
|
|
1612
|
+
verticalSpan: spans.vertical,
|
|
1613
|
+
manhattanSpan: spans.manhattan,
|
|
1614
|
+
thresholds: {
|
|
1615
|
+
horizontalSpan: LONG_CONNECTOR_HORIZONTAL_SPAN,
|
|
1616
|
+
verticalSpan: LONG_CONNECTOR_VERTICAL_SPAN,
|
|
1617
|
+
manhattanSpan: LONG_CONNECTOR_MANHATTAN_SPAN,
|
|
1618
|
+
},
|
|
1619
|
+
},
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
function siblingCohortAnchor(context, cohort) {
|
|
1624
|
+
const focus = context.focuses.get(cohort[0] ?? '');
|
|
1625
|
+
const coordinates = cohort
|
|
1626
|
+
.flatMap((focusId) => {
|
|
1627
|
+
const node = context.placed.get(focusId);
|
|
1628
|
+
return node === undefined ? [] : [node.x];
|
|
1629
|
+
})
|
|
1630
|
+
.sort((left, right) => left - right);
|
|
1631
|
+
if (coordinates.length >= 4 &&
|
|
1632
|
+
(coordinates.at(-1) ?? 0) - (coordinates[0] ?? 0) > 2 * LONG_CONNECTOR_HORIZONTAL_SPAN) {
|
|
1633
|
+
return {
|
|
1634
|
+
anchorX: ((coordinates[0] ?? 0) + (coordinates.at(-1) ?? 0)) / 2,
|
|
1635
|
+
anchorKind: 'subtree_envelope',
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
if (focus !== undefined) {
|
|
1639
|
+
const parents = prerequisiteIds(focus).flatMap((focusId) => {
|
|
1640
|
+
const parent = context.placed.get(focusId);
|
|
1641
|
+
return parent === undefined ? [] : [parent];
|
|
1642
|
+
});
|
|
1643
|
+
if (parents.length > 0) {
|
|
1644
|
+
return {
|
|
1645
|
+
anchorX: structuralAnchorX(context, focus, parents, context.laneIds.get(focus.id) ?? 'default'),
|
|
1646
|
+
anchorKind: parents.length > 1 && focus.convergence ? 'parent_average' : 'parent_median',
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
const laneId = focus === undefined ? 'default' : (context.laneIds.get(focus.id) ?? 'default');
|
|
1651
|
+
return { anchorX: context.laneBases.get(laneId) ?? 0, anchorKind: 'lane_base' };
|
|
1652
|
+
}
|
|
1653
|
+
function* siblingSymmetryDiagnostics(context) {
|
|
1654
|
+
let asymmetricSiblingCohortCount = 0;
|
|
1655
|
+
let totalSiblingDeviation = 0;
|
|
1656
|
+
let maximumSiblingDeviation = 0;
|
|
1657
|
+
let offAnchorSiblingCohortCount = 0;
|
|
1658
|
+
let totalSiblingAnchorDeviation = 0;
|
|
1659
|
+
let maximumSiblingAnchorDeviation = 0;
|
|
1660
|
+
for (const [cohortIndex, cohort] of context.siblingCohorts.entries()) {
|
|
1661
|
+
yield* cancellationCheckpoint(context.signal, cohortIndex, 16);
|
|
1662
|
+
const nodes = cohort
|
|
1663
|
+
.flatMap((focusId) => {
|
|
1664
|
+
const node = context.placed.get(focusId);
|
|
1665
|
+
return node === undefined ? [] : [node];
|
|
1666
|
+
})
|
|
1667
|
+
.sort((left, right) => left.x - right.x || compareCodeUnits(left.id, right.id));
|
|
1668
|
+
if (nodes.length < 2)
|
|
1669
|
+
continue;
|
|
1670
|
+
const { anchorX, anchorKind } = siblingCohortAnchor(context, cohort);
|
|
1671
|
+
const anchorCenterTwice = 2 * anchorX;
|
|
1672
|
+
const mirrorCenterTwice = (nodes[0]?.x ?? 0) + (nodes.at(-1)?.x ?? 0);
|
|
1673
|
+
let mirrorDeviation = 0;
|
|
1674
|
+
let anchorDeviation = 0;
|
|
1675
|
+
for (let index = 0; index <= Math.floor((nodes.length - 1) / 2); index += 1) {
|
|
1676
|
+
const left = nodes[index];
|
|
1677
|
+
const right = nodes[nodes.length - 1 - index];
|
|
1678
|
+
if (left === undefined || right === undefined)
|
|
1679
|
+
continue;
|
|
1680
|
+
const pairSum = left.x + right.x;
|
|
1681
|
+
mirrorDeviation += Math.abs(pairSum - mirrorCenterTwice);
|
|
1682
|
+
anchorDeviation += Math.abs(pairSum - anchorCenterTwice);
|
|
1683
|
+
}
|
|
1684
|
+
totalSiblingDeviation += mirrorDeviation;
|
|
1685
|
+
maximumSiblingDeviation = Math.max(maximumSiblingDeviation, mirrorDeviation);
|
|
1686
|
+
totalSiblingAnchorDeviation += anchorDeviation;
|
|
1687
|
+
maximumSiblingAnchorDeviation = Math.max(maximumSiblingAnchorDeviation, anchorDeviation);
|
|
1688
|
+
const focus = context.focuses.get(nodes.at(-1)?.id ?? '');
|
|
1689
|
+
if (mirrorDeviation > 0) {
|
|
1690
|
+
asymmetricSiblingCohortCount += 1;
|
|
1691
|
+
context.diagnostics.push({
|
|
1692
|
+
code: 'FOCUS_LAYOUT_SIBLING_ASYMMETRY',
|
|
1693
|
+
severity: 'warning',
|
|
1694
|
+
category: 'layout',
|
|
1695
|
+
message: `Sibling cohort ${cohort.join(', ')} is not mirror-symmetric`,
|
|
1696
|
+
...(focus?.sourceLocation === undefined ? {} : { location: focus.sourceLocation }),
|
|
1697
|
+
details: {
|
|
1698
|
+
focusIds: cohort,
|
|
1699
|
+
xCoordinates: nodes.map(({ x }) => x),
|
|
1700
|
+
centerTwice: mirrorCenterTwice,
|
|
1701
|
+
deviation: mirrorDeviation,
|
|
1702
|
+
},
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
if (anchorDeviation > 0) {
|
|
1706
|
+
offAnchorSiblingCohortCount += 1;
|
|
1707
|
+
context.diagnostics.push({
|
|
1708
|
+
code: 'FOCUS_LAYOUT_SIBLING_ANCHOR_DEVIATION',
|
|
1709
|
+
severity: 'warning',
|
|
1710
|
+
category: 'layout',
|
|
1711
|
+
message: `Sibling cohort ${cohort.join(', ')} is offset from its structural anchor`,
|
|
1712
|
+
...(focus?.sourceLocation === undefined ? {} : { location: focus.sourceLocation }),
|
|
1713
|
+
details: {
|
|
1714
|
+
focusIds: cohort,
|
|
1715
|
+
xCoordinates: nodes.map(({ x }) => x),
|
|
1716
|
+
centerTwice: anchorCenterTwice,
|
|
1717
|
+
anchorX,
|
|
1718
|
+
anchorKind,
|
|
1719
|
+
deviation: anchorDeviation,
|
|
1720
|
+
},
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return {
|
|
1725
|
+
asymmetricSiblingCohortCount,
|
|
1726
|
+
totalSiblingDeviation,
|
|
1727
|
+
maximumSiblingDeviation,
|
|
1728
|
+
offAnchorSiblingCohortCount,
|
|
1729
|
+
totalSiblingAnchorDeviation,
|
|
1730
|
+
maximumSiblingAnchorDeviation,
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
function layoutMetrics(context, edges, crossingCount, nodeIntersectionCount, symmetry, spacing) {
|
|
1734
|
+
const nodes = [...context.placed.values()];
|
|
1735
|
+
const xCoordinates = nodes.map(({ x }) => x);
|
|
1736
|
+
const yCoordinates = nodes.map(({ y }) => y);
|
|
1737
|
+
const minimumX = xCoordinates.length === 0 ? 0 : Math.min(...xCoordinates);
|
|
1738
|
+
const maximumX = xCoordinates.length === 0 ? 0 : Math.max(...xCoordinates);
|
|
1739
|
+
const minimumY = yCoordinates.length === 0 ? 0 : Math.min(...yCoordinates);
|
|
1740
|
+
const maximumY = yCoordinates.length === 0 ? 0 : Math.max(...yCoordinates);
|
|
1741
|
+
const spans = edges.map(connectorSpans);
|
|
1742
|
+
const sum = (values) => values.reduce((total, value) => total + value, 0);
|
|
1743
|
+
const maximum = (values) => values.length === 0 ? 0 : Math.max(...values);
|
|
1744
|
+
return {
|
|
1745
|
+
bounds: {
|
|
1746
|
+
minimumX,
|
|
1747
|
+
maximumX,
|
|
1748
|
+
minimumY,
|
|
1749
|
+
maximumY,
|
|
1750
|
+
columnSpan: maximumX - minimumX,
|
|
1751
|
+
rowSpan: maximumY - minimumY,
|
|
1752
|
+
columnCount: nodes.length === 0 ? 0 : maximumX - minimumX + 1,
|
|
1753
|
+
rowCount: nodes.length === 0 ? 0 : maximumY - minimumY + 1,
|
|
1754
|
+
},
|
|
1755
|
+
connectors: {
|
|
1756
|
+
count: edges.length,
|
|
1757
|
+
crossingCount,
|
|
1758
|
+
nodeIntersectionCount,
|
|
1759
|
+
longConnectorCount: edges.filter(longConnector).length,
|
|
1760
|
+
totalHorizontalSpan: sum(spans.map(({ horizontal }) => horizontal)),
|
|
1761
|
+
maximumHorizontalSpan: maximum(spans.map(({ horizontal }) => horizontal)),
|
|
1762
|
+
totalVerticalSpan: sum(spans.map(({ vertical }) => vertical)),
|
|
1763
|
+
maximumVerticalSpan: maximum(spans.map(({ vertical }) => vertical)),
|
|
1764
|
+
totalManhattanSpan: sum(spans.map(({ manhattan }) => manhattan)),
|
|
1765
|
+
maximumManhattanSpan: maximum(spans.map(({ manhattan }) => manhattan)),
|
|
1766
|
+
},
|
|
1767
|
+
symmetry: {
|
|
1768
|
+
siblingCohortCount: context.siblingCohorts.length,
|
|
1769
|
+
...symmetry,
|
|
1770
|
+
boundingCenterOffsetTwice: Math.abs(minimumX + maximumX),
|
|
1771
|
+
},
|
|
1772
|
+
spacing: {
|
|
1773
|
+
requiredSameRowSpacing: context.nodeSpacing,
|
|
1774
|
+
...spacing,
|
|
1775
|
+
},
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
629
1778
|
function* parentOrderDiagnostics(context) {
|
|
630
1779
|
let focusIndex = 0;
|
|
631
1780
|
for (const focus of context.focuses.values()) {
|
|
@@ -717,23 +1866,28 @@ function* layoutFocusTreeSteps(plan, options = {}) {
|
|
|
717
1866
|
}
|
|
718
1867
|
const laneIds = new Map([...focuses.values()].map((focus) => [focus.id, laneFor(plan, focus)]));
|
|
719
1868
|
const discoveredLanes = [...new Set(laneIds.values())].sort((left, right) => compareCodeUnits(left, right));
|
|
1869
|
+
const nodeSpacing = options.nodeSpacing ?? DEFAULT_NODE_SPACING;
|
|
1870
|
+
const siblingLayout = automaticSiblingLayout(plan, focuses, laneIds, nodeSpacing);
|
|
720
1871
|
const context = {
|
|
721
1872
|
plan,
|
|
722
1873
|
focuses,
|
|
723
1874
|
previous: new Map(options.previous?.nodes.map((node) => [node.id, node]) ?? []),
|
|
724
1875
|
placed: new Map(),
|
|
725
1876
|
occupied: new Map(),
|
|
1877
|
+
occupiedRows: new Map(),
|
|
726
1878
|
connectorDefinitionsByFocus: connectorDefinitionsByFocus(focuses),
|
|
727
1879
|
placedConnectors: new Map(),
|
|
728
1880
|
mutualExclusions: mutualExclusionMap(focuses),
|
|
729
|
-
laneBases: laneBases(plan, discoveredLanes, options.laneSpacing ??
|
|
1881
|
+
laneBases: laneBases(plan, discoveredLanes, options.laneSpacing ?? DEFAULT_LANE_SPACING),
|
|
730
1882
|
laneBounds: laneBounds(plan),
|
|
731
1883
|
laneIds,
|
|
732
|
-
|
|
733
|
-
|
|
1884
|
+
siblingOffsets: siblingLayout.offsets,
|
|
1885
|
+
siblingCohorts: siblingLayout.cohorts,
|
|
1886
|
+
nodeSpacing,
|
|
1887
|
+
minimumMutualExclusionSpacing: nodeSpacing,
|
|
734
1888
|
decisions: [],
|
|
735
1889
|
diagnostics,
|
|
736
|
-
work: new
|
|
1890
|
+
work: options.workBudget ?? new FocusLayoutWorkBudget(),
|
|
737
1891
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
738
1892
|
};
|
|
739
1893
|
const ordered = [...focuses.values()].sort((left, right) => {
|
|
@@ -750,11 +1904,20 @@ function* layoutFocusTreeSteps(plan, options = {}) {
|
|
|
750
1904
|
yield* cancellationCheckpoint(options.signal, focusIndex, 8);
|
|
751
1905
|
yield* placeFocus(context, focus.id);
|
|
752
1906
|
}
|
|
1907
|
+
yield* repairConnectorCrossings(context);
|
|
1908
|
+
yield* refineConnectorQuality(context);
|
|
1909
|
+
centerFullyAutomaticLayout(context);
|
|
753
1910
|
yield* parentOrderDiagnostics(context);
|
|
754
1911
|
yield* laneBoundsDiagnostics(context);
|
|
755
1912
|
yield* visibleOverlapDiagnostics(context);
|
|
1913
|
+
const spacing = yield* sameRowSpacingDiagnostics(context);
|
|
756
1914
|
yield* mutualExclusionSpacingDiagnostics(context);
|
|
757
|
-
yield*
|
|
1915
|
+
const edges = yield* connectorEdges(context);
|
|
1916
|
+
const crossingSummary = yield* connectorCrossings(edges, context);
|
|
1917
|
+
const nodeIntersectionCount = yield* connectorNodeIntersectionDiagnostics(context, edges);
|
|
1918
|
+
yield* connectorCrossingDiagnostics(context, crossingSummary.samples);
|
|
1919
|
+
yield* connectorLengthDiagnostics(context, edges);
|
|
1920
|
+
const symmetry = yield* siblingSymmetryDiagnostics(context);
|
|
758
1921
|
const nodes = [...context.placed.values()].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
759
1922
|
const decisions = [...context.decisions].sort((left, right) => compareCodeUnits(left.focusId, right.focusId));
|
|
760
1923
|
yield* cancellationCheckpoint(options.signal);
|
|
@@ -763,6 +1926,7 @@ function* layoutFocusTreeSteps(plan, options = {}) {
|
|
|
763
1926
|
nodes,
|
|
764
1927
|
decisions,
|
|
765
1928
|
diagnostics: sortDiagnostics(context.diagnostics.values()),
|
|
1929
|
+
metrics: layoutMetrics(context, edges, crossingSummary.count, nodeIntersectionCount, symmetry, spacing),
|
|
766
1930
|
layoutHash: hashCanonical(nodes.map(({ id, x, y, laneId }) => ({ id, laneId, x, y }))),
|
|
767
1931
|
};
|
|
768
1932
|
}
|
|
@@ -773,4 +1937,3 @@ export function layoutFocusTree(plan, options = {}) {
|
|
|
773
1937
|
export async function layoutFocusTreeAsync(plan, options = {}) {
|
|
774
1938
|
return completeCooperatively(layoutFocusTreeSteps(plan, options), options.signal);
|
|
775
1939
|
}
|
|
776
|
-
//# sourceMappingURL=layout.js.map
|