Match the web's text, connector and fill rendering

10ab3fb · Gabriel Morell · 2026-09-16 03:37

23 files +1778 -169
Message
{commit_body(@commit)}

Files changed

modified android/app/src/main/java/org/neiam/diogramos/app/canvas/CanvasDocument.kt
+17 −6
@@ -147,13 +147,15 @@ data class Style(
147 147 val stroke: String? = null,
148 148 val shadow: Boolean = false,
149 149 val dash: String = "solid",
150 − val strokeWidth: Double = 1.0,
150 + val strokeWidth: Double = 1.5,
151 151 val fontFamily: String? = null,
152 152 val fontSize: Double? = null,
153 153 val fontBold: Boolean = false,
154 154 val fontItalic: Boolean = false,
155 − val labelPosition: String = "center",
156 − val labelSize: String = "md",
155 + // Null, not "center": label_geom/1 defaults a circle to center and
156 + // everything else to top-left, which only the renderer knows.
157 + val labelPosition: String? = null,
158 + val labelSize: String? = null,
157 159 val labelColor: String? = null,
158 160 val labelFontFamily: String? = null,
159 161 val labelBold: Boolean = false,
@@ -167,13 +169,13 @@ data class Style(
167 169 stroke = json["stroke"].asStringOrNull(),
168 170 shadow = json["shadow"].asBooleanOrNull() ?: false,
169 171 dash = json["dash"].asStringOrNull() ?: "solid",
170 − strokeWidth = json["stroke_width"].asDoubleOrNull() ?: 1.0,
172 + strokeWidth = json["stroke_width"].asDoubleOrNull() ?: 1.5,
171 173 fontFamily = json["font_family"].asStringOrNull(),
172 174 fontSize = json["font_size"].asDoubleOrNull(),
173 175 fontBold = json["font_bold"].asBooleanOrNull() ?: false,
174 176 fontItalic = json["font_italic"].asBooleanOrNull() ?: false,
175 − labelPosition = json["label_position"].asStringOrNull() ?: "center",
176 − labelSize = json["label_size"].asStringOrNull() ?: "md",
177 + labelPosition = json["label_position"].asStringOrNull(),
178 + labelSize = json["label_size"].asStringOrNull(),
177 179 labelColor = json["label_color"].asStringOrNull(),
178 180 labelFontFamily = json["label_font_family"].asStringOrNull(),
179 181 labelBold = json["label_bold"].asBooleanOrNull() ?: false,
@@ -188,6 +190,14 @@ data class Anchor(
188 190 val side: String = "auto",
189 191 val x: Double? = null,
190 192 val y: Double? = null,
193 + /**
194 + * The element at the *other* end, written by the web editor when a
195 + * connector is created. An "auto" anchor picks the edge facing this
196 + * element; without it there is nothing to aim at and the server's
197 + * own fallback compares an element's centre with itself, which
198 + * always resolves east.
199 + */
200 + val other: String? = null,
191 201 ) {
192 202 companion object {
193 203 fun parse(json: JsonObject?): Anchor? {
@@ -198,6 +208,7 @@ data class Anchor(
198 208 side = json["anchor"].asStringOrNull() ?: "auto",
199 209 x = json["x"].asDoubleOrNull(),
200 210 y = json["y"].asDoubleOrNull(),
211 + other = json["_other"].asStringOrNull(),
201 212 )
202 213 }
203 214 }
modified android/app/src/main/java/org/neiam/diogramos/app/canvas/CanvasScreen.kt
+35 −6
@@ -91,6 +91,7 @@ fun CanvasScreen(
91 91 var live by remember { mutableStateOf(LiveState.Connecting) }
92 92 var peers by remember { mutableStateOf<List<Peer>>(emptyList()) }
93 93 var sender by remember { mutableStateOf<CanvasSender?>(null) }
94 + var liveError by remember { mutableStateOf<String?>(null) }
94 95 var selectedId by remember { mutableStateOf<String?>(null) }
95 96 var drag by remember { mutableStateOf<DragState?>(null) }
96 97 var showAddSheet by remember { mutableStateOf(false) }
@@ -142,7 +143,19 @@ fun CanvasScreen(
142 143 val job: Job = scope.launch {
143 144 CanvasSocket(creds).subscribe(canvasId).collectLatest { event ->
144 145 when (event) {
145 − is CanvasEvent.Connected -> Unit // Wait for the join.
146 + // The first connect waits for the join and its
147 + // snapshot. A later one is a reconnect: the client
148 + // rejoins the channel by itself without firing the
149 + // join callback again, so this is the only signal
150 + // that traffic is flowing again -- and the only
151 + // chance to resync, since that silent rejoin brings
152 + // no snapshot with it.
153 + is CanvasEvent.Connected -> {
154 + if (live != LiveState.Connecting) {
155 + live = LiveState.Live
156 + load()
157 + }
158 + }
146 159
147 160 is CanvasEvent.Disconnected -> {
148 161 live = LiveState.Offline
@@ -163,6 +176,10 @@ fun CanvasScreen(
163 176 }
164 177
165 178 is CanvasEvent.Op -> {
179 + // An op is proof the channel is up, whatever the
180 + // banner last heard.
181 + live = LiveState.Live
182 +
166 183 when (session.onRemoteOp(event.op, event.version, event.actorRef)) {
167 184 CanvasSession.Outcome.Applied -> revision++
168 185
@@ -177,12 +194,18 @@ fun CanvasScreen(
177 194 }
178 195 }
179 196
180 − is CanvasEvent.Peers -> peers = event.peers
197 + is CanvasEvent.Peers -> {
198 + peers = event.peers
199 + live = LiveState.Live
200 + }
181 201
182 202 is CanvasEvent.Failed -> {
183 203 live = LiveState.Offline
184 204 // A failure here costs liveness, not the diagram,
185 − // so the fetched document stays on screen.
205 + // so the fetched document stays on screen -- but
206 + // the reason belongs on screen too, or "Offline"
207 + // is all anyone can report back.
208 + liveError = event.message
186 209 }
187 210 }
188 211 }
@@ -288,7 +311,7 @@ fun CanvasScreen(
288 311 },
289 312 ) { padding ->
290 313 Column(modifier = Modifier.fillMaxSize().padding(padding)) {
291 − LiveBanner(live, peers, meta?.canWrite ?: true)
314 + LiveBanner(live, liveError, peers, meta?.canWrite ?: true)
292 315
293 316 Box(modifier = Modifier.fillMaxSize()) {
294 317 when {
@@ -487,10 +510,16 @@ private suspend fun SnackbarHostState.showMessage(message: String) {
487 510 }
488 511
489 512 @Composable
490 −private fun LiveBanner(live: LiveState, peers: List<Peer>, canWrite: Boolean) {
513 +private fun LiveBanner(
514 + live: LiveState,
515 + error: String?,
516 + peers: List<Peer>,
517 + canWrite: Boolean,
518 +) {
491 519 val message = when {
492 520 live == LiveState.Connecting -> "Connecting…"
493 − live == LiveState.Offline -> "Offline — this view won't update until it reconnects."
521 + live == LiveState.Offline ->
522 + error ?: "Offline — this view won't update until it reconnects."
494 523 !canWrite -> "Read-only" + peersSuffix(peers)
495 524 peers.isNotEmpty() -> peers.joinToString(", ") { it.name } + " also here"
496 525 else -> null
modified android/app/src/main/java/org/neiam/diogramos/app/canvas/ConnectorGeometry.kt
+13 −6
@@ -122,12 +122,19 @@ object ConnectorGeometry {
122 122
123 123 "n", "e", "s", "w" -> edge(element.bbox, anchor.side)
124 124
125 − // "auto" on the server resolves against `_other`, which nothing
126 − // populates, so `best_edge` compares the element's centre with
127 − // itself and always lands on the east edge. Reproduced
128 − // deliberately: the fixtures encode it, and the port has to
129 − // match the web until the server side is fixed.
130 − "auto" -> bestEdge(element.bbox, Point(element.bbox.centerX, element.bbox.centerY))
125 + // Aim at the other endpoint, which is what `_other` carries.
126 + // When it is absent or names an element that is gone, the
127 + // server compares the element's centre with itself and the
128 + // first branch of best_edge wins, so every such anchor
129 + // resolves east -- matched here rather than second-guessed.
130 + "auto" -> {
131 + val target = anchor.other
132 + ?.let { document.elements[it] }
133 + ?.let { Point(it.bbox.centerX, it.bbox.centerY) }
134 + ?: Point(element.bbox.centerX, element.bbox.centerY)
135 +
136 + bestEdge(element.bbox, target)
137 + }
131 138
132 139 else -> null
133 140 }
modified android/app/src/main/java/org/neiam/diogramos/app/canvas/Renderer.kt
+136 −96
@@ -15,8 +15,6 @@ import androidx.compose.ui.text.TextStyle
15 15 import androidx.compose.ui.text.font.FontFamily
16 16 import androidx.compose.ui.text.font.FontStyle
17 17 import androidx.compose.ui.text.font.FontWeight
18 −import androidx.compose.ui.text.style.TextAlign
19 −import androidx.compose.ui.unit.Constraints
20 18
21 19 import org.neiam.diogramos.app.ui.theme.AppTheme
22 20 import org.neiam.diogramos.app.ui.theme.B612
@@ -110,9 +108,11 @@ class Renderer(
110 108
111 109 // A shape someone else is dragging is ghosted rather than drawn
112 110 // at a stale position, the same way the web editor does it.
113 − val alpha = if (dimmed) 0.35f else 1f
114 − val fill = element.style.fill?.let { theme.token(it).copy(alpha = alpha) }
115 − val stroke = element.style.stroke?.let { theme.token(it).copy(alpha = alpha) }
111 + // `shape_style/1` fills an unstyled shape with base-200 and
112 + // strokes it with base-content; leaving them unpainted here drew
113 + // outlines where the web draws solid boxes.
114 + val fill = theme.token(element.style.fill ?: "base-200").ghost(dimmed)
115 + val stroke = theme.token(element.style.stroke ?: "base-content").ghost(dimmed)
116 116 val strokeWidth = (element.style.strokeWidth * viewport.scale).toFloat()
117 117 val dash = dashEffect(element.style.dash, viewport.scale)
118 118
@@ -121,44 +121,43 @@ class Renderer(
121 121 val radius = ((element.radius ?: 0.0) * viewport.scale).toFloat()
122 122 val center = viewport.toScreen(element.bbox.centerX, element.bbox.centerY)
123 123
124 − fill?.let { scope.drawCircle(it, radius, center) }
125 − stroke?.let {
126 − scope.drawCircle(
127 − it,
128 − radius,
129 − center,
130 − style = Stroke(width = strokeWidth, pathEffect = dash),
131 − )
132 − }
124 + scope.drawCircle(fill, radius, center)
125 + scope.drawCircle(
126 + stroke,
127 + radius,
128 + center,
129 + style = Stroke(width = strokeWidth, pathEffect = dash),
130 + )
133 131 }
134 132
135 133 ElementType.ROUNDED -> {
136 134 val corner = (8f * viewport.scale).coerceAtMost(size.minDimension / 2)
137 135 val radius = androidx.compose.ui.geometry.CornerRadius(corner, corner)
138 136
139 − fill?.let { scope.drawRoundRect(it, topLeft, size, radius) }
140 − stroke?.let {
141 − scope.drawRoundRect(
142 − it,
143 − topLeft,
144 − size,
145 − radius,
146 − style = Stroke(width = strokeWidth, pathEffect = dash),
147 − )
148 − }
137 + scope.drawRoundRect(fill, topLeft, size, radius)
138 + scope.drawRoundRect(
139 + stroke,
140 + topLeft,
141 + size,
142 + radius,
143 + style = Stroke(width = strokeWidth, pathEffect = dash),
144 + )
149 145 }
150 146
151 − ElementType.RECT, ElementType.TEXT -> {
152 − fill?.let { scope.drawRect(it, topLeft, size) }
153 − stroke?.let {
154 − scope.drawRect(
155 − it,
156 − topLeft,
157 − size,
158 − style = Stroke(width = strokeWidth, pathEffect = dash),
159 − )
160 − }
147 + ElementType.RECT -> {
148 + scope.drawRect(fill, topLeft, size)
149 + scope.drawRect(
150 + stroke,
151 + topLeft,
152 + size,
153 + style = Stroke(width = strokeWidth, pathEffect = dash),
154 + )
161 155 }
156 +
157 + // A text element is a bare <text> on the web -- its box is
158 + // only an anchor, never painted, and `fill` is the colour of
159 + // the glyphs rather than of a rectangle.
160 + ElementType.TEXT -> Unit
162 161 }
163 162
164 163 if (element.label.isNotEmpty()) {
@@ -167,8 +166,13 @@ class Renderer(
167 166 }
168 167
169 168 /**
170 − * A text element's body fills its box; every other type carries a
171 − * label positioned in one of the nine slots the web offers.
169 + * Text, laid out the way the web's SVG does it.
170 + *
171 + * The critical part is that there is no wrapping: `<text>` is a
172 + * single line that overflows its box when it is too long. Measuring
173 + * against the box width instead made every label that didn't fit
174 + * break between characters and render as a vertical column of
175 + * letters.
172 176 */
173 177 private fun drawLabel(
174 178 scope: DrawScope,
@@ -177,81 +181,99 @@ class Renderer(
177 181 size: Size,
178 182 viewport: Viewport,
179 183 ) {
180 − val isText = element.type == ElementType.TEXT
181 184 val style = element.style
182 185
183 − // Document units scaled by the zoom, so this is device pixels.
184 − val fontSizePx = when {
185 − isText -> (style.fontSize ?: 16.0) * viewport.scale
186 − else -> labelSizePx(style.labelSize) * viewport.scale
187 − }
188 − if (fontSizePx < 4f) return // Below this it is noise, not text.
189 −
190 − // px -> sp, deliberately. `sp` is scaled by both display density
191 − // and the reader's font-size setting, so handing it a pixel count
192 − // renders the text density-times too large -- and since the wrap
193 − // width below is in real pixels, every word then overflows its box
194 − // and the label wraps after each character.
195 − //
196 − // This is also why canvas text must not follow the system font
197 − // scale: a diagram's text has to fit the box it was drawn in, the
198 − // same way `font_size` is in user units on the web.
199 − val fontSize = with(scope) { fontSizePx.toFloat().toSp() }
200 −
201 − val color = when {
202 − isText -> style.fill?.let { theme.token(it) } ?: theme.baseContent
203 − else -> style.labelColor?.let { theme.token(it) } ?: theme.baseContent
186 + if (element.type == ElementType.TEXT) {
187 + // text_element_style/1: fill defaults to base-content, size to 14.
188 + val fontSizePx = (style.fontSize ?: DEFAULT_TEXT_SIZE) * viewport.scale
189 + if (fontSizePx < MIN_LEGIBLE_PX) return
190 +
191 + val layout = measure(
192 + scope = scope,
193 + text = element.label,
194 + fontSizePx = fontSizePx,
195 + color = theme.token(style.fill ?: "base-content"),
196 + family = style.fontFamily,
197 + bold = style.fontBold,
198 + italic = style.fontItalic,
199 + )
200 +
201 + // text_baseline/1 puts the baseline at y + round(size * 0.85)
202 + // so the top of the glyph lands on y.
203 + val baselineY = element.bbox.y + Math.round((style.fontSize ?: DEFAULT_TEXT_SIZE) * 0.85)
204 + val anchor = viewport.toScreen(element.bbox.x, baselineY.toDouble())
205 +
206 + scope.drawText(
207 + layout,
208 + topLeft = Offset(anchor.x, anchor.y - layout.firstBaseline),
209 + )
210 + return
204 211 }
205 212
206 − val layout = textMeasurer.measure(
213 + // label_geom/1 for a shape.
214 + val fontSizePx = labelSize(style.labelSize) * viewport.scale
215 + if (fontSizePx < MIN_LEGIBLE_PX) return
216 +
217 + val layout = measure(
218 + scope = scope,
207 219 text = element.label,
208 − style = TextStyle(
209 − color = color,
210 − fontSize = fontSize,
211 − fontFamily = fontFamily(if (isText) style.fontFamily else style.labelFontFamily),
212 − fontWeight = if (if (isText) style.fontBold else style.labelBold) {
213 − FontWeight.Bold
214 − } else {
215 − FontWeight.Normal
216 − },
217 − fontStyle = if (if (isText) style.fontItalic else style.labelItalic) {
218 − FontStyle.Italic
219 − } else {
220 − FontStyle.Normal
221 − },
222 − textAlign = TextAlign.Center,
223 − ),
224 − constraints = Constraints(maxWidth = size.width.toInt().coerceAtLeast(1)),
220 + fontSizePx = fontSizePx,
221 + color = theme.token(style.labelColor ?: "base-content"),
222 + family = style.labelFontFamily,
223 + bold = style.labelBold,
224 + italic = style.labelItalic,
225 225 )
226 226
227 − val position = if (isText) "center" else style.labelPosition
228 − scope.drawText(layout, topLeft = labelOffset(position, topLeft, size, layout))
229 − }
230 −
231 − private fun labelOffset(
232 − position: String,
233 − topLeft: Offset,
234 − size: Size,
235 − layout: TextLayoutResult,
236 − ): Offset {
237 − val textWidth = layout.size.width.toFloat()
238 − val textHeight = layout.size.height.toFloat()
227 + // A circle's label centres; everything else sits top-left.
228 + val position = style.labelPosition
229 + ?: if (element.type == ElementType.CIRCLE) "center" else "tl"
230 + val pad = (LABEL_PADDING * viewport.scale).toFloat()
239 231
240 232 val x = when (position) {
241 − "tl", "ml", "bl" -> topLeft.x
242 − "tr", "mr", "br" -> topLeft.x + size.width - textWidth
243 − else -> topLeft.x + (size.width - textWidth) / 2
233 + "tl", "ml", "bl" -> topLeft.x + pad
234 + "tr", "mr", "br" -> topLeft.x + size.width - pad - layout.size.width
235 + else -> topLeft.x + (size.width - layout.size.width) / 2
244 236 }
245 237
246 238 val y = when (position) {
247 − "tl", "tc", "tr" -> topLeft.y
248 − "bl", "bc", "br" -> topLeft.y + size.height - textHeight
249 − else -> topLeft.y + (size.height - textHeight) / 2
239 + // "hanging": the top of the text sits on the anchor.
240 + "tl", "tc", "tr" -> topLeft.y + pad
241 + // "auto": the alphabetic baseline sits on the anchor.
242 + "bl", "bc", "br" -> topLeft.y + size.height - pad - layout.firstBaseline
243 + else -> topLeft.y + (size.height - layout.size.height) / 2
250 244 }
251 245
252 − return Offset(x, y)
246 + scope.drawText(layout, topLeft = Offset(x, y))
253 247 }
254 248
249 + private fun measure(
250 + scope: DrawScope,
251 + text: String,
252 + fontSizePx: Double,
253 + color: Color,
254 + family: String?,
255 + bold: Boolean,
256 + italic: Boolean,
257 + ): TextLayoutResult = textMeasurer.measure(
258 + text = text,
259 + style = TextStyle(
260 + color = color,
261 + // px -> sp, deliberately. `sp` is scaled by display density
262 + // and by the reader's font-size setting, so handing it a pixel
263 + // count renders text density-times too large. Canvas text must
264 + // not follow the system font scale either: it has to fit the
265 + // box it was drawn in, as `font_size` does in user units on
266 + // the web.
267 + fontSize = with(scope) { fontSizePx.toFloat().toSp() },
268 + fontFamily = fontFamily(family),
269 + fontWeight = if (bold) FontWeight.Bold else FontWeight.Normal,
270 + fontStyle = if (italic) FontStyle.Italic else FontStyle.Normal,
271 + ),
272 + // SVG <text> is one line that overflows rather than wrapping.
273 + softWrap = false,
274 + maxLines = 1,
275 + )
276 +
255 277 private fun fontFamily(token: String?): FontFamily = when (token) {
256 278 "serif" -> FontFamily.Serif
257 279 "mono" -> FontFamily.Monospace
@@ -259,7 +281,7 @@ class Renderer(
259 281 else -> FontFamily.SansSerif
260 282 }
261 283
262 − private fun labelSizePx(token: String): Double = when (token) {
284 + private fun labelSize(token: String?): Double = when (token) {
263 285 "sm" -> 11.0
264 286 "lg" -> 18.0
265 287 "xl" -> 24.0
@@ -393,9 +415,27 @@ class Renderer(
393 415
394 416 private companion object {
395 417 const val ARROW_SPREAD = 0.5f
418 +
419 + /** `text_element_style/1` and `label_geom/1`. */
420 + const val DEFAULT_TEXT_SIZE = 14.0
421 + const val LABEL_PADDING = 8.0
422 +
423 + /** Below this a glyph is noise; skip it rather than draw fuzz. */
424 + const val MIN_LEGIBLE_PX = 4.0
396 425 }
397 426 }
398 427
428 +/**
429 + * Fades a colour for a shape someone else is dragging.
430 + *
431 + * Scales the alpha it already has rather than replacing it, because
432 + * `Color.Transparent` is black at zero alpha: setting alpha to 1 on it
433 + * turns "no fill" into a solid black box, which is exactly what every
434 + * `fill: "transparent"` container rendered as.
435 + */
436 +private fun Color.ghost(dimmed: Boolean): Color =
437 + if (dimmed) copy(alpha = alpha * 0.35f) else this
438 +
399 439 /** Screen-space radius of a corner handle: a finger, not a pixel. */
400 440 const val HANDLE_RADIUS_PX = 14f
401 441
modified android/app/src/test/resources/connector_geometry.json
+1545 −33
@@ -814,6 +814,132 @@
814 814 },
815 815 "name": "rects-apart/straight/e->point"
816 816 },
817 + {
818 + "connector": {
819 + "color": "base-content",
820 + "dash": "solid",
821 + "end_gap": 6,
822 + "from": {
823 + "_other": "b",
824 + "anchor": "auto",
825 + "element": "a"
826 + },
827 + "id": "k",
828 + "marker_end": "arrow",
829 + "marker_start": "none",
830 + "routing": "straight",
831 + "start_gap": 6,
832 + "stroke_width": 2,
833 + "to": {
834 + "_other": "a",
835 + "anchor": "auto",
836 + "element": "b"
837 + }
838 + },
839 + "document": "rects-apart",
840 + "expected": {
841 + "d": "M 106 30 L 294 240",
842 + "dash_array": null,
843 + "marker_end": "arrow",
844 + "marker_start": "none",
845 + "stroke_width": 2
846 + },
847 + "name": "rects-apart/straight/auto->auto"
848 + },
849 + {
850 + "connector": {
851 + "color": "base-content",
852 + "dash": "solid",
853 + "end_gap": 6,
854 + "from": {
855 + "_other": "b",
856 + "anchor": "auto",
857 + "element": "a"
858 + },
859 + "id": "k",
860 + "marker_end": "arrow",
861 + "marker_start": "none",
862 + "routing": "straight",
863 + "start_gap": 6,
864 + "stroke_width": 2,
865 + "to": {
866 + "anchor": "w",
867 + "element": "b"
868 + }
869 + },
870 + "document": "rects-apart",
871 + "expected": {
872 + "d": "M 106 30 L 294 240",
873 + "dash_array": null,
874 + "marker_end": "arrow",
875 + "marker_start": "none",
876 + "stroke_width": 2
877 + },
878 + "name": "rects-apart/straight/auto->w"
879 + },
880 + {
881 + "connector": {
882 + "color": "base-content",
883 + "dash": "solid",
884 + "end_gap": 6,
885 + "from": {
886 + "anchor": "n",
887 + "element": "a"
888 + },
889 + "id": "k",
890 + "marker_end": "arrow",
891 + "marker_start": "none",
892 + "routing": "straight",
893 + "start_gap": 6,
894 + "stroke_width": 2,
895 + "to": {
896 + "_other": "a",
897 + "anchor": "auto",
898 + "element": "b"
899 + }
900 + },
901 + "document": "rects-apart",
902 + "expected": {
903 + "d": "M 50 -6 L 294 240",
904 + "dash_array": null,
905 + "marker_end": "arrow",
906 + "marker_start": "none",
907 + "stroke_width": 2
908 + },
909 + "name": "rects-apart/straight/n->auto"
910 + },
911 + {
912 + "connector": {
913 + "color": "base-content",
914 + "dash": "solid",
915 + "end_gap": 6,
916 + "from": {
917 + "_other": "ghost",
918 + "anchor": "auto",
919 + "element": "a"
920 + },
921 + "id": "k",
922 + "marker_end": "arrow",
923 + "marker_start": "none",
924 + "routing": "straight",
925 + "start_gap": 6,
926 + "stroke_width": 2,
927 + "to": {
928 + "_other": "a",
929 + "anchor": "auto",
930 + "element": "b"
931 + }
932 + },
933 + "document": "rects-apart",
934 + "expected": {
935 + "d": "M 106 30 L 294 240",
936 + "dash_array": null,
937 + "marker_end": "arrow",
938 + "marker_start": "none",
939 + "stroke_width": 2
940 + },
941 + "name": "rects-apart/straight/auto->auto"
942 + },
817 943 {
818 944 "connector": {
819 945 "color": "base-content",
@@ -1628,6 +1754,132 @@
1628 1754 },
1629 1755 "name": "rects-apart/orthogonal/e->point"
1630 1756 },
1757 + {
1758 + "connector": {
1759 + "color": "base-content",
1760 + "dash": "solid",
1761 + "end_gap": 6,
1762 + "from": {
1763 + "_other": "b",
1764 + "anchor": "auto",
1765 + "element": "a"
1766 + },
1767 + "id": "k",
1768 + "marker_end": "arrow",
1769 + "marker_start": "none",
1770 + "routing": "orthogonal",
1771 + "start_gap": 6,
1772 + "stroke_width": 2,
1773 + "to": {
1774 + "_other": "a",
1775 + "anchor": "auto",
1776 + "element": "b"
1777 + }
1778 + },
1779 + "document": "rects-apart",
1780 + "expected": {
1781 + "d": "M 106 30 L 200 30 L 200 240 L 294 240",
1782 + "dash_array": null,
1783 + "marker_end": "arrow",
1784 + "marker_start": "none",
1785 + "stroke_width": 2
1786 + },
1787 + "name": "rects-apart/orthogonal/auto->auto"
1788 + },
1789 + {
1790 + "connector": {
1791 + "color": "base-content",
1792 + "dash": "solid",
1793 + "end_gap": 6,
1794 + "from": {
1795 + "_other": "b",
1796 + "anchor": "auto",
1797 + "element": "a"
1798 + },
1799 + "id": "k",
1800 + "marker_end": "arrow",
1801 + "marker_start": "none",
1802 + "routing": "orthogonal",
1803 + "start_gap": 6,
1804 + "stroke_width": 2,
1805 + "to": {
1806 + "anchor": "w",
1807 + "element": "b"
1808 + }
1809 + },
1810 + "document": "rects-apart",
1811 + "expected": {
1812 + "d": "M 106 30 L 200 30 L 200 240 L 294 240",
1813 + "dash_array": null,
1814 + "marker_end": "arrow",
1815 + "marker_start": "none",
1816 + "stroke_width": 2
1817 + },
1818 + "name": "rects-apart/orthogonal/auto->w"
1819 + },
1820 + {
1821 + "connector": {
1822 + "color": "base-content",
1823 + "dash": "solid",
1824 + "end_gap": 6,
1825 + "from": {
1826 + "anchor": "n",
1827 + "element": "a"
1828 + },
1829 + "id": "k",
1830 + "marker_end": "arrow",
1831 + "marker_start": "none",
1832 + "routing": "orthogonal",
1833 + "start_gap": 6,
1834 + "stroke_width": 2,
1835 + "to": {
1836 + "_other": "a",
1837 + "anchor": "auto",
1838 + "element": "b"
1839 + }
1840 + },
1841 + "document": "rects-apart",
1842 + "expected": {
1843 + "d": "M 50 -6 L 50 117 L 294 117 L 294 240",
1844 + "dash_array": null,
1845 + "marker_end": "arrow",
1846 + "marker_start": "none",
1847 + "stroke_width": 2
1848 + },
1849 + "name": "rects-apart/orthogonal/n->auto"
1850 + },
1851 + {
1852 + "connector": {
1853 + "color": "base-content",
1854 + "dash": "solid",
1855 + "end_gap": 6,
1856 + "from": {
1857 + "_other": "ghost",
1858 + "anchor": "auto",
1859 + "element": "a"
1860 + },
1861 + "id": "k",
1862 + "marker_end": "arrow",
1863 + "marker_start": "none",
1864 + "routing": "orthogonal",
1865 + "start_gap": 6,
1866 + "stroke_width": 2,
1867 + "to": {
1868 + "_other": "a",
1869 + "anchor": "auto",
1870 + "element": "b"
1871 + }
1872 + },
1873 + "document": "rects-apart",
1874 + "expected": {
1875 + "d": "M 106 30 L 200 30 L 200 240 L 294 240",
1876 + "dash_array": null,
1877 + "marker_end": "arrow",
1878 + "marker_start": "none",
1879 + "stroke_width": 2
1880 + },
1881 + "name": "rects-apart/orthogonal/auto->auto"
1882 + },
1631 1883 {
1632 1884 "connector": {
1633 1885 "color": "base-content",
@@ -2446,37 +2698,163 @@
2446 2698 "connector": {
2447 2699 "color": "base-content",
2448 2700 "dash": "solid",
2449 − "end_gap": 0,
2701 + "end_gap": 6,
2450 2702 "from": {
2451 − "anchor": "e",
2703 + "_other": "b",
2704 + "anchor": "auto",
2452 2705 "element": "a"
2453 2706 },
2454 2707 "id": "k",
2455 2708 "marker_end": "arrow",
2456 2709 "marker_start": "none",
2457 − "routing": "straight",
2458 − "start_gap": 0,
2710 + "routing": "curve",
2711 + "start_gap": 6,
2459 2712 "stroke_width": 2,
2460 2713 "to": {
2461 − "anchor": "w",
2714 + "_other": "a",
2715 + "anchor": "auto",
2462 2716 "element": "b"
2463 2717 }
2464 2718 },
2465 2719 "document": "rects-apart",
2466 2720 "expected": {
2467 − "d": "M 100 30 L 300 240",
2721 + "d": "M 106 30 C 218.74 30, 181.26 240, 294 240",
2468 2722 "dash_array": null,
2469 2723 "marker_end": "arrow",
2470 2724 "marker_start": "none",
2471 2725 "stroke_width": 2
2472 2726 },
2473 − "name": "rects-apart/straight/e->w"
2727 + "name": "rects-apart/curve/auto->auto"
2474 2728 },
2475 2729 {
2476 2730 "connector": {
2477 2731 "color": "base-content",
2478 2732 "dash": "solid",
2479 − "end_gap": 3.5,
2733 + "end_gap": 6,
2734 + "from": {
2735 + "_other": "b",
2736 + "anchor": "auto",
2737 + "element": "a"
2738 + },
2739 + "id": "k",
2740 + "marker_end": "arrow",
2741 + "marker_start": "none",
2742 + "routing": "curve",
2743 + "start_gap": 6,
2744 + "stroke_width": 2,
2745 + "to": {
2746 + "anchor": "w",
2747 + "element": "b"
2748 + }
2749 + },
2750 + "document": "rects-apart",
2751 + "expected": {
2752 + "d": "M 106 30 C 218.74 30, 181.26 240, 294 240",
2753 + "dash_array": null,
2754 + "marker_end": "arrow",
2755 + "marker_start": "none",
2756 + "stroke_width": 2
2757 + },
2758 + "name": "rects-apart/curve/auto->w"
2759 + },
2760 + {
2761 + "connector": {
2762 + "color": "base-content",
2763 + "dash": "solid",
2764 + "end_gap": 6,
2765 + "from": {
2766 + "anchor": "n",
2767 + "element": "a"
2768 + },
2769 + "id": "k",
2770 + "marker_end": "arrow",
2771 + "marker_start": "none",
2772 + "routing": "curve",
2773 + "start_gap": 6,
2774 + "stroke_width": 2,
2775 + "to": {
2776 + "_other": "a",
2777 + "anchor": "auto",
2778 + "element": "b"
2779 + }
2780 + },
2781 + "document": "rects-apart",
2782 + "expected": {
2783 + "d": "M 50 -6 C 50 -144.59, 155.41 240, 294 240",
2784 + "dash_array": null,
2785 + "marker_end": "arrow",
2786 + "marker_start": "none",
2787 + "stroke_width": 2
2788 + },
2789 + "name": "rects-apart/curve/n->auto"
2790 + },
2791 + {
2792 + "connector": {
2793 + "color": "base-content",
2794 + "dash": "solid",
2795 + "end_gap": 6,
2796 + "from": {
2797 + "_other": "ghost",
2798 + "anchor": "auto",
2799 + "element": "a"
2800 + },
2801 + "id": "k",
2802 + "marker_end": "arrow",
2803 + "marker_start": "none",
2804 + "routing": "curve",
2805 + "start_gap": 6,
2806 + "stroke_width": 2,
2807 + "to": {
2808 + "_other": "a",
2809 + "anchor": "auto",
2810 + "element": "b"
2811 + }
2812 + },
2813 + "document": "rects-apart",
2814 + "expected": {
2815 + "d": "M 106 30 C 218.74 30, 181.26 240, 294 240",
2816 + "dash_array": null,
2817 + "marker_end": "arrow",
2818 + "marker_start": "none",
2819 + "stroke_width": 2
2820 + },
2821 + "name": "rects-apart/curve/auto->auto"
2822 + },
2823 + {
2824 + "connector": {
2825 + "color": "base-content",
2826 + "dash": "solid",
2827 + "end_gap": 0,
2828 + "from": {
2829 + "anchor": "e",
2830 + "element": "a"
2831 + },
2832 + "id": "k",
2833 + "marker_end": "arrow",
2834 + "marker_start": "none",
2835 + "routing": "straight",
2836 + "start_gap": 0,
2837 + "stroke_width": 2,
2838 + "to": {
2839 + "anchor": "w",
2840 + "element": "b"
2841 + }
2842 + },
2843 + "document": "rects-apart",
2844 + "expected": {
2845 + "d": "M 100 30 L 300 240",
2846 + "dash_array": null,
2847 + "marker_end": "arrow",
2848 + "marker_start": "none",
2849 + "stroke_width": 2
2850 + },
2851 + "name": "rects-apart/straight/e->w"
2852 + },
2853 + {
2854 + "connector": {
2855 + "color": "base-content",
2856 + "dash": "solid",
2857 + "end_gap": 3.5,
2480 2858 "from": {
2481 2859 "anchor": "e",
2482 2860 "element": "a"
@@ -3518,6 +3896,132 @@
3518 3896 },
3519 3897 "name": "circle-and-rect/straight/e->point"
3520 3898 },
3899 + {
3900 + "connector": {
3901 + "color": "base-content",
3902 + "dash": "solid",
3903 + "end_gap": 6,
3904 + "from": {
3905 + "_other": "b",
3906 + "anchor": "auto",
3907 + "element": "a"
3908 + },
3909 + "id": "k",
3910 + "marker_end": "arrow",
3911 + "marker_start": "none",
3912 + "routing": "straight",
3913 + "start_gap": 6,
3914 + "stroke_width": 2,
3915 + "to": {
3916 + "_other": "a",
3917 + "anchor": "auto",
3918 + "element": "b"
3919 + }
3920 + },
3921 + "document": "circle-and-rect",
3922 + "expected": {
3923 + "d": "M 4 50 L -154 -95",
3924 + "dash_array": null,
3925 + "marker_end": "arrow",
3926 + "marker_start": "none",
3927 + "stroke_width": 2
3928 + },
3929 + "name": "circle-and-rect/straight/auto->auto"
3930 + },
3931 + {
3932 + "connector": {
3933 + "color": "base-content",
3934 + "dash": "solid",
3935 + "end_gap": 6,
3936 + "from": {
3937 + "_other": "b",
3938 + "anchor": "auto",
3939 + "element": "a"
3940 + },
3941 + "id": "k",
3942 + "marker_end": "arrow",
3943 + "marker_start": "none",
3944 + "routing": "straight",
3945 + "start_gap": 6,
3946 + "stroke_width": 2,
3947 + "to": {
3948 + "anchor": "w",
3949 + "element": "b"
3950 + }
3951 + },
3952 + "document": "circle-and-rect",
3953 + "expected": {
3954 + "d": "M 4 50 L -256 -95",
3955 + "dash_array": null,
3956 + "marker_end": "arrow",
3957 + "marker_start": "none",
3958 + "stroke_width": 2
3959 + },
3960 + "name": "circle-and-rect/straight/auto->w"
3961 + },
3962 + {
3963 + "connector": {
3964 + "color": "base-content",
3965 + "dash": "solid",
3966 + "end_gap": 6,
3967 + "from": {
3968 + "anchor": "n",
3969 + "element": "a"
3970 + },
3971 + "id": "k",
3972 + "marker_end": "arrow",
3973 + "marker_start": "none",
3974 + "routing": "straight",
3975 + "start_gap": 6,
3976 + "stroke_width": 2,
3977 + "to": {
3978 + "_other": "a",
3979 + "anchor": "auto",
3980 + "element": "b"
3981 + }
3982 + },
3983 + "document": "circle-and-rect",
3984 + "expected": {
3985 + "d": "M 50 4 L -154 -95",
3986 + "dash_array": null,
3987 + "marker_end": "arrow",
3988 + "marker_start": "none",
3989 + "stroke_width": 2
3990 + },
3991 + "name": "circle-and-rect/straight/n->auto"
3992 + },
3993 + {
3994 + "connector": {
3995 + "color": "base-content",
3996 + "dash": "solid",
3997 + "end_gap": 6,
3998 + "from": {
3999 + "_other": "ghost",
4000 + "anchor": "auto",
4001 + "element": "a"
4002 + },
4003 + "id": "k",
4004 + "marker_end": "arrow",
4005 + "marker_start": "none",
4006 + "routing": "straight",
4007 + "start_gap": 6,
4008 + "stroke_width": 2,
4009 + "to": {
4010 + "_other": "a",
4011 + "anchor": "auto",
4012 + "element": "b"
4013 + }
4014 + },
4015 + "document": "circle-and-rect",
4016 + "expected": {
4017 + "d": "M 96 50 L -154 -95",
4018 + "dash_array": null,
4019 + "marker_end": "arrow",
4020 + "marker_start": "none",
4021 + "stroke_width": 2
4022 + },
4023 + "name": "circle-and-rect/straight/auto->auto"
4024 + },
3521 4025 {
3522 4026 "connector": {
3523 4027 "color": "base-content",
@@ -4332,6 +4836,132 @@
4332 4836 },
4333 4837 "name": "circle-and-rect/orthogonal/e->point"
4334 4838 },
4839 + {
4840 + "connector": {
4841 + "color": "base-content",
4842 + "dash": "solid",
4843 + "end_gap": 6,
4844 + "from": {
4845 + "_other": "b",
4846 + "anchor": "auto",
4847 + "element": "a"
4848 + },
4849 + "id": "k",
4850 + "marker_end": "arrow",
4851 + "marker_start": "none",
4852 + "routing": "orthogonal",
4853 + "start_gap": 6,
4854 + "stroke_width": 2,
4855 + "to": {
4856 + "_other": "a",
4857 + "anchor": "auto",
4858 + "element": "b"
4859 + }
4860 + },
4861 + "document": "circle-and-rect",
4862 + "expected": {
4863 + "d": "M 4 50 L -75 50 L -75 -95 L -154 -95",
4864 + "dash_array": null,
4865 + "marker_end": "arrow",
4866 + "marker_start": "none",
4867 + "stroke_width": 2
4868 + },
4869 + "name": "circle-and-rect/orthogonal/auto->auto"
4870 + },
4871 + {
4872 + "connector": {
4873 + "color": "base-content",
4874 + "dash": "solid",
4875 + "end_gap": 6,
4876 + "from": {
4877 + "_other": "b",
4878 + "anchor": "auto",
4879 + "element": "a"
4880 + },
4881 + "id": "k",
4882 + "marker_end": "arrow",
4883 + "marker_start": "none",
4884 + "routing": "orthogonal",
4885 + "start_gap": 6,
4886 + "stroke_width": 2,
4887 + "to": {
4888 + "anchor": "w",
4889 + "element": "b"
4890 + }
4891 + },
4892 + "document": "circle-and-rect",
4893 + "expected": {
4894 + "d": "M 4 50 L -126 50 L -126 -95 L -256 -95",
4895 + "dash_array": null,
4896 + "marker_end": "arrow",
4897 + "marker_start": "none",
4898 + "stroke_width": 2
4899 + },
4900 + "name": "circle-and-rect/orthogonal/auto->w"
4901 + },
4902 + {
4903 + "connector": {
4904 + "color": "base-content",
4905 + "dash": "solid",
4906 + "end_gap": 6,
4907 + "from": {
4908 + "anchor": "n",
4909 + "element": "a"
4910 + },
4911 + "id": "k",
4912 + "marker_end": "arrow",
4913 + "marker_start": "none",
4914 + "routing": "orthogonal",
4915 + "start_gap": 6,
4916 + "stroke_width": 2,
4917 + "to": {
4918 + "_other": "a",
4919 + "anchor": "auto",
4920 + "element": "b"
4921 + }
4922 + },
4923 + "document": "circle-and-rect",
4924 + "expected": {
4925 + "d": "M 50 4 L 50 -45.5 L -154 -45.5 L -154 -95",
4926 + "dash_array": null,
4927 + "marker_end": "arrow",
4928 + "marker_start": "none",
4929 + "stroke_width": 2
4930 + },
4931 + "name": "circle-and-rect/orthogonal/n->auto"
4932 + },
4933 + {
4934 + "connector": {
4935 + "color": "base-content",
4936 + "dash": "solid",
4937 + "end_gap": 6,
4938 + "from": {
4939 + "_other": "ghost",
4940 + "anchor": "auto",
4941 + "element": "a"
4942 + },
4943 + "id": "k",
4944 + "marker_end": "arrow",
4945 + "marker_start": "none",
4946 + "routing": "orthogonal",
4947 + "start_gap": 6,
4948 + "stroke_width": 2,
4949 + "to": {
4950 + "_other": "a",
4951 + "anchor": "auto",
4952 + "element": "b"
4953 + }
4954 + },
4955 + "document": "circle-and-rect",
4956 + "expected": {
4957 + "d": "M 96 50 L -29 50 L -29 -95 L -154 -95",
4958 + "dash_array": null,
4959 + "marker_end": "arrow",
4960 + "marker_start": "none",
4961 + "stroke_width": 2
4962 + },
4963 + "name": "circle-and-rect/orthogonal/auto->auto"
4964 + },
4335 4965 {
4336 4966 "connector": {
4337 4967 "color": "base-content",
@@ -5112,7 +5742,133 @@
5112 5742 "marker_start": "none",
5113 5743 "stroke_width": 2
5114 5744 },
5115 − "name": "circle-and-rect/curve/point->e"
5745 + "name": "circle-and-rect/curve/point->e"
5746 + },
5747 + {
5748 + "connector": {
5749 + "color": "base-content",
5750 + "dash": "solid",
5751 + "end_gap": 6,
5752 + "from": {
5753 + "anchor": "e",
5754 + "element": "a"
5755 + },
5756 + "id": "k",
5757 + "marker_end": "arrow",
5758 + "marker_start": "none",
5759 + "routing": "curve",
5760 + "start_gap": 6,
5761 + "stroke_width": 2,
5762 + "to": {
5763 + "anchor": "point",
5764 + "element": "b",
5765 + "x": 400,
5766 + "y": 333.75
5767 + }
5768 + },
5769 + "document": "circle-and-rect",
5770 + "expected": {
5771 + "d": "M 96 50 C 262.34 50, 400 333.75, 400 333.75",
5772 + "dash_array": null,
5773 + "marker_end": "arrow",
5774 + "marker_start": "none",
5775 + "stroke_width": 2
5776 + },
5777 + "name": "circle-and-rect/curve/e->point"
5778 + },
5779 + {
5780 + "connector": {
5781 + "color": "base-content",
5782 + "dash": "solid",
5783 + "end_gap": 6,
5784 + "from": {
5785 + "_other": "b",
5786 + "anchor": "auto",
5787 + "element": "a"
5788 + },
5789 + "id": "k",
5790 + "marker_end": "arrow",
5791 + "marker_start": "none",
5792 + "routing": "curve",
5793 + "start_gap": 6,
5794 + "stroke_width": 2,
5795 + "to": {
5796 + "_other": "a",
5797 + "anchor": "auto",
5798 + "element": "b"
5799 + }
5800 + },
5801 + "document": "circle-and-rect",
5802 + "expected": {
5803 + "d": "M 4 50 C -81.78 50, -68.22 -95, -154 -95",
5804 + "dash_array": null,
5805 + "marker_end": "arrow",
5806 + "marker_start": "none",
5807 + "stroke_width": 2
5808 + },
5809 + "name": "circle-and-rect/curve/auto->auto"
5810 + },
5811 + {
5812 + "connector": {
5813 + "color": "base-content",
5814 + "dash": "solid",
5815 + "end_gap": 6,
5816 + "from": {
5817 + "_other": "b",
5818 + "anchor": "auto",
5819 + "element": "a"
5820 + },
5821 + "id": "k",
5822 + "marker_end": "arrow",
5823 + "marker_start": "none",
5824 + "routing": "curve",
5825 + "start_gap": 6,
5826 + "stroke_width": 2,
5827 + "to": {
5828 + "anchor": "w",
5829 + "element": "b"
5830 + }
5831 + },
5832 + "document": "circle-and-rect",
5833 + "expected": {
5834 + "d": "M 4 50 C -115.08 50, -375.08 -95, -256 -95",
5835 + "dash_array": null,
5836 + "marker_end": "arrow",
5837 + "marker_start": "none",
5838 + "stroke_width": 2
5839 + },
5840 + "name": "circle-and-rect/curve/auto->w"
5841 + },
5842 + {
5843 + "connector": {
5844 + "color": "base-content",
5845 + "dash": "solid",
5846 + "end_gap": 6,
5847 + "from": {
5848 + "anchor": "n",
5849 + "element": "a"
5850 + },
5851 + "id": "k",
5852 + "marker_end": "arrow",
5853 + "marker_start": "none",
5854 + "routing": "curve",
5855 + "start_gap": 6,
5856 + "stroke_width": 2,
5857 + "to": {
5858 + "_other": "a",
5859 + "anchor": "auto",
5860 + "element": "b"
5861 + }
5862 + },
5863 + "document": "circle-and-rect",
5864 + "expected": {
5865 + "d": "M 50 4 C 50 -86.7, -63.3 -95, -154 -95",
5866 + "dash_array": null,
5867 + "marker_end": "arrow",
5868 + "marker_start": "none",
5869 + "stroke_width": 2
5870 + },
5871 + "name": "circle-and-rect/curve/n->auto"
5116 5872 },
5117 5873 {
5118 5874 "connector": {
@@ -5120,7 +5876,8 @@
5120 5876 "dash": "solid",
5121 5877 "end_gap": 6,
5122 5878 "from": {
5123 − "anchor": "e",
5879 + "_other": "ghost",
5880 + "anchor": "auto",
5124 5881 "element": "a"
5125 5882 },
5126 5883 "id": "k",
@@ -5130,21 +5887,20 @@
5130 5887 "start_gap": 6,
5131 5888 "stroke_width": 2,
5132 5889 "to": {
5133 − "anchor": "point",
5134 − "element": "b",
5135 − "x": 400,
5136 − "y": 333.75
5890 + "_other": "a",
5891 + "anchor": "auto",
5892 + "element": "b"
5137 5893 }
5138 5894 },
5139 5895 "document": "circle-and-rect",
5140 5896 "expected": {
5141 − "d": "M 96 50 C 262.34 50, 400 333.75, 400 333.75",
5897 + "d": "M 96 50 C 211.6 50, -38.4 -95, -154 -95",
5142 5898 "dash_array": null,
5143 5899 "marker_end": "arrow",
5144 5900 "marker_start": "none",
5145 5901 "stroke_width": 2
5146 5902 },
5147 − "name": "circle-and-rect/curve/e->point"
5903 + "name": "circle-and-rect/curve/auto->auto"
5148 5904 },
5149 5905 {
5150 5906 "connector": {
@@ -6222,6 +6978,132 @@
6222 6978 },
6223 6979 "name": "fractional/straight/e->point"
6224 6980 },
6981 + {
6982 + "connector": {
6983 + "color": "base-content",
6984 + "dash": "solid",
6985 + "end_gap": 6,
6986 + "from": {
6987 + "_other": "b",
6988 + "anchor": "auto",
6989 + "element": "a"
6990 + },
6991 + "id": "k",
6992 + "marker_end": "arrow",
6993 + "marker_start": "none",
6994 + "routing": "straight",
6995 + "start_gap": 6,
6996 + "stroke_width": 2,
6997 + "to": {
6998 + "_other": "a",
6999 + "anchor": "auto",
7000 + "element": "b"
7001 + }
7002 + },
7003 + "document": "fractional",
7004 + "expected": {
7005 + "d": "M 40.5 10.75 L 95.33 11",
7006 + "dash_array": null,
7007 + "marker_end": "arrow",
7008 + "marker_start": "none",
7009 + "stroke_width": 2
7010 + },
7011 + "name": "fractional/straight/auto->auto"
7012 + },
7013 + {
7014 + "connector": {
7015 + "color": "base-content",
7016 + "dash": "solid",
7017 + "end_gap": 6,
7018 + "from": {
7019 + "_other": "b",
7020 + "anchor": "auto",
7021 + "element": "a"
7022 + },
7023 + "id": "k",
7024 + "marker_end": "arrow",
7025 + "marker_start": "none",
7026 + "routing": "straight",
7027 + "start_gap": 6,
7028 + "stroke_width": 2,
7029 + "to": {
7030 + "anchor": "w",
7031 + "element": "b"
7032 + }
7033 + },
7034 + "document": "fractional",
7035 + "expected": {
7036 + "d": "M 40.5 10.75 L 95.33 11",
7037 + "dash_array": null,
7038 + "marker_end": "arrow",
7039 + "marker_start": "none",
7040 + "stroke_width": 2
7041 + },
7042 + "name": "fractional/straight/auto->w"
7043 + },
7044 + {
7045 + "connector": {
7046 + "color": "base-content",
7047 + "dash": "solid",
7048 + "end_gap": 6,
7049 + "from": {
7050 + "anchor": "n",
7051 + "element": "a"
7052 + },
7053 + "id": "k",
7054 + "marker_end": "arrow",
7055 + "marker_start": "none",
7056 + "routing": "straight",
7057 + "start_gap": 6,
7058 + "stroke_width": 2,
7059 + "to": {
7060 + "_other": "a",
7061 + "anchor": "auto",
7062 + "element": "b"
7063 + }
7064 + },
7065 + "document": "fractional",
7066 + "expected": {
7067 + "d": "M 18 -3.75 L 95.33 11",
7068 + "dash_array": null,
7069 + "marker_end": "arrow",
7070 + "marker_start": "none",
7071 + "stroke_width": 2
7072 + },
7073 + "name": "fractional/straight/n->auto"
7074 + },
7075 + {
7076 + "connector": {
7077 + "color": "base-content",
7078 + "dash": "solid",
7079 + "end_gap": 6,
7080 + "from": {
7081 + "_other": "ghost",
7082 + "anchor": "auto",
7083 + "element": "a"
7084 + },
7085 + "id": "k",
7086 + "marker_end": "arrow",
7087 + "marker_start": "none",
7088 + "routing": "straight",
7089 + "start_gap": 6,
7090 + "stroke_width": 2,
7091 + "to": {
7092 + "_other": "a",
7093 + "anchor": "auto",
7094 + "element": "b"
7095 + }
7096 + },
7097 + "document": "fractional",
7098 + "expected": {
7099 + "d": "M 40.5 10.75 L 95.33 11",
7100 + "dash_array": null,
7101 + "marker_end": "arrow",
7102 + "marker_start": "none",
7103 + "stroke_width": 2
7104 + },
7105 + "name": "fractional/straight/auto->auto"
7106 + },
6225 7107 {
6226 7108 "connector": {
6227 7109 "color": "base-content",
@@ -7002,7 +7884,133 @@
7002 7884 "marker_start": "none",
7003 7885 "stroke_width": 2
7004 7886 },
7005 − "name": "fractional/orthogonal/point->e"
7887 + "name": "fractional/orthogonal/point->e"
7888 + },
7889 + {
7890 + "connector": {
7891 + "color": "base-content",
7892 + "dash": "solid",
7893 + "end_gap": 6,
7894 + "from": {
7895 + "anchor": "e",
7896 + "element": "a"
7897 + },
7898 + "id": "k",
7899 + "marker_end": "arrow",
7900 + "marker_start": "none",
7901 + "routing": "orthogonal",
7902 + "start_gap": 6,
7903 + "stroke_width": 2,
7904 + "to": {
7905 + "anchor": "point",
7906 + "element": "b",
7907 + "x": 400,
7908 + "y": 333.75
7909 + }
7910 + },
7911 + "document": "fractional",
7912 + "expected": {
7913 + "d": "M 40.5 10.75 L 220.25 10.75 L 220.25 333.75 L 400 333.75",
7914 + "dash_array": null,
7915 + "marker_end": "arrow",
7916 + "marker_start": "none",
7917 + "stroke_width": 2
7918 + },
7919 + "name": "fractional/orthogonal/e->point"
7920 + },
7921 + {
7922 + "connector": {
7923 + "color": "base-content",
7924 + "dash": "solid",
7925 + "end_gap": 6,
7926 + "from": {
7927 + "_other": "b",
7928 + "anchor": "auto",
7929 + "element": "a"
7930 + },
7931 + "id": "k",
7932 + "marker_end": "arrow",
7933 + "marker_start": "none",
7934 + "routing": "orthogonal",
7935 + "start_gap": 6,
7936 + "stroke_width": 2,
7937 + "to": {
7938 + "_other": "a",
7939 + "anchor": "auto",
7940 + "element": "b"
7941 + }
7942 + },
7943 + "document": "fractional",
7944 + "expected": {
7945 + "d": "M 40.5 10.75 L 67.92 10.75 L 67.92 11 L 95.33 11",
7946 + "dash_array": null,
7947 + "marker_end": "arrow",
7948 + "marker_start": "none",
7949 + "stroke_width": 2
7950 + },
7951 + "name": "fractional/orthogonal/auto->auto"
7952 + },
7953 + {
7954 + "connector": {
7955 + "color": "base-content",
7956 + "dash": "solid",
7957 + "end_gap": 6,
7958 + "from": {
7959 + "_other": "b",
7960 + "anchor": "auto",
7961 + "element": "a"
7962 + },
7963 + "id": "k",
7964 + "marker_end": "arrow",
7965 + "marker_start": "none",
7966 + "routing": "orthogonal",
7967 + "start_gap": 6,
7968 + "stroke_width": 2,
7969 + "to": {
7970 + "anchor": "w",
7971 + "element": "b"
7972 + }
7973 + },
7974 + "document": "fractional",
7975 + "expected": {
7976 + "d": "M 40.5 10.75 L 67.92 10.75 L 67.92 11 L 95.33 11",
7977 + "dash_array": null,
7978 + "marker_end": "arrow",
7979 + "marker_start": "none",
7980 + "stroke_width": 2
7981 + },
7982 + "name": "fractional/orthogonal/auto->w"
7983 + },
7984 + {
7985 + "connector": {
7986 + "color": "base-content",
7987 + "dash": "solid",
7988 + "end_gap": 6,
7989 + "from": {
7990 + "anchor": "n",
7991 + "element": "a"
7992 + },
7993 + "id": "k",
7994 + "marker_end": "arrow",
7995 + "marker_start": "none",
7996 + "routing": "orthogonal",
7997 + "start_gap": 6,
7998 + "stroke_width": 2,
7999 + "to": {
8000 + "_other": "a",
8001 + "anchor": "auto",
8002 + "element": "b"
8003 + }
8004 + },
8005 + "document": "fractional",
8006 + "expected": {
8007 + "d": "M 18 -3.75 L 18 3.63 L 95.33 3.63 L 95.33 11",
8008 + "dash_array": null,
8009 + "marker_end": "arrow",
8010 + "marker_start": "none",
8011 + "stroke_width": 2
8012 + },
8013 + "name": "fractional/orthogonal/n->auto"
7006 8014 },
7007 8015 {
7008 8016 "connector": {
@@ -7010,7 +8018,8 @@
7010 8018 "dash": "solid",
7011 8019 "end_gap": 6,
7012 8020 "from": {
7013 − "anchor": "e",
8021 + "_other": "ghost",
8022 + "anchor": "auto",
7014 8023 "element": "a"
7015 8024 },
7016 8025 "id": "k",
@@ -7020,21 +8029,20 @@
7020 8029 "start_gap": 6,
7021 8030 "stroke_width": 2,
7022 8031 "to": {
7023 − "anchor": "point",
7024 − "element": "b",
7025 − "x": 400,
7026 − "y": 333.75
8032 + "_other": "a",
8033 + "anchor": "auto",
8034 + "element": "b"
7027 8035 }
7028 8036 },
7029 8037 "document": "fractional",
7030 8038 "expected": {
7031 − "d": "M 40.5 10.75 L 220.25 10.75 L 220.25 333.75 L 400 333.75",
8039 + "d": "M 40.5 10.75 L 67.92 10.75 L 67.92 11 L 95.33 11",
7032 8040 "dash_array": null,
7033 8041 "marker_end": "arrow",
7034 8042 "marker_start": "none",
7035 8043 "stroke_width": 2
7036 8044 },
7037 − "name": "fractional/orthogonal/e->point"
8045 + "name": "fractional/orthogonal/auto->auto"
7038 8046 },
7039 8047 {
7040 8048 "connector": {
@@ -7850,6 +8858,132 @@
7850 8858 },
7851 8859 "name": "fractional/curve/e->point"
7852 8860 },
8861 + {
8862 + "connector": {
8863 + "color": "base-content",
8864 + "dash": "solid",
8865 + "end_gap": 6,
8866 + "from": {
8867 + "_other": "b",
8868 + "anchor": "auto",
8869 + "element": "a"
8870 + },
8871 + "id": "k",
8872 + "marker_end": "arrow",
8873 + "marker_start": "none",
8874 + "routing": "curve",
8875 + "start_gap": 6,
8876 + "stroke_width": 2,
8877 + "to": {
8878 + "_other": "a",
8879 + "anchor": "auto",
8880 + "element": "b"
8881 + }
8882 + },
8883 + "document": "fractional",
8884 + "expected": {
8885 + "d": "M 40.5 10.75 C 62.43 10.75, 73.4 11, 95.33 11",
8886 + "dash_array": null,
8887 + "marker_end": "arrow",
8888 + "marker_start": "none",
8889 + "stroke_width": 2
8890 + },
8891 + "name": "fractional/curve/auto->auto"
8892 + },
8893 + {
8894 + "connector": {
8895 + "color": "base-content",
8896 + "dash": "solid",
8897 + "end_gap": 6,
8898 + "from": {
8899 + "_other": "b",
8900 + "anchor": "auto",
8901 + "element": "a"
8902 + },
8903 + "id": "k",
8904 + "marker_end": "arrow",
8905 + "marker_start": "none",
8906 + "routing": "curve",
8907 + "start_gap": 6,
8908 + "stroke_width": 2,
8909 + "to": {
8910 + "anchor": "w",
8911 + "element": "b"
8912 + }
8913 + },
8914 + "document": "fractional",
8915 + "expected": {
8916 + "d": "M 40.5 10.75 C 62.43 10.75, 73.4 11, 95.33 11",
8917 + "dash_array": null,
8918 + "marker_end": "arrow",
8919 + "marker_start": "none",
8920 + "stroke_width": 2
8921 + },
8922 + "name": "fractional/curve/auto->w"
8923 + },
8924 + {
8925 + "connector": {
8926 + "color": "base-content",
8927 + "dash": "solid",
8928 + "end_gap": 6,
8929 + "from": {
8930 + "anchor": "n",
8931 + "element": "a"
8932 + },
8933 + "id": "k",
8934 + "marker_end": "arrow",
8935 + "marker_start": "none",
8936 + "routing": "curve",
8937 + "start_gap": 6,
8938 + "stroke_width": 2,
8939 + "to": {
8940 + "_other": "a",
8941 + "anchor": "auto",
8942 + "element": "b"
8943 + }
8944 + },
8945 + "document": "fractional",
8946 + "expected": {
8947 + "d": "M 18 -3.75 C 18 -35.24, 63.84 11, 95.33 11",
8948 + "dash_array": null,
8949 + "marker_end": "arrow",
8950 + "marker_start": "none",
8951 + "stroke_width": 2
8952 + },
8953 + "name": "fractional/curve/n->auto"
8954 + },
8955 + {
8956 + "connector": {
8957 + "color": "base-content",
8958 + "dash": "solid",
8959 + "end_gap": 6,
8960 + "from": {
8961 + "_other": "ghost",
8962 + "anchor": "auto",
8963 + "element": "a"
8964 + },
8965 + "id": "k",
8966 + "marker_end": "arrow",
8967 + "marker_start": "none",
8968 + "routing": "curve",
8969 + "start_gap": 6,
8970 + "stroke_width": 2,
8971 + "to": {
8972 + "_other": "a",
8973 + "anchor": "auto",
8974 + "element": "b"
8975 + }
8976 + },
8977 + "document": "fractional",
8978 + "expected": {
8979 + "d": "M 40.5 10.75 C 62.43 10.75, 73.4 11, 95.33 11",
8980 + "dash_array": null,
8981 + "marker_end": "arrow",
8982 + "marker_start": "none",
8983 + "stroke_width": 2
8984 + },
8985 + "name": "fractional/curve/auto->auto"
8986 + },
7853 8987 {
7854 8988 "connector": {
7855 8989 "color": "base-content",
@@ -8886,13 +10020,139 @@
8886 10020 },
8887 10021 "document": "overlapping",
8888 10022 "expected": {
8889 − "d": "M 12.5 -3 L 126 70",
10023 + "d": "M 12.5 -3 L 126 70",
10024 + "dash_array": null,
10025 + "marker_end": "arrow",
10026 + "marker_start": "none",
10027 + "stroke_width": 2
10028 + },
10029 + "name": "overlapping/straight/point->e"
10030 + },
10031 + {
10032 + "connector": {
10033 + "color": "base-content",
10034 + "dash": "solid",
10035 + "end_gap": 6,
10036 + "from": {
10037 + "anchor": "e",
10038 + "element": "a"
10039 + },
10040 + "id": "k",
10041 + "marker_end": "arrow",
10042 + "marker_start": "none",
10043 + "routing": "straight",
10044 + "start_gap": 6,
10045 + "stroke_width": 2,
10046 + "to": {
10047 + "anchor": "point",
10048 + "element": "b",
10049 + "x": 400,
10050 + "y": 333.75
10051 + }
10052 + },
10053 + "document": "overlapping",
10054 + "expected": {
10055 + "d": "M 106 50 L 400 333.75",
10056 + "dash_array": null,
10057 + "marker_end": "arrow",
10058 + "marker_start": "none",
10059 + "stroke_width": 2
10060 + },
10061 + "name": "overlapping/straight/e->point"
10062 + },
10063 + {
10064 + "connector": {
10065 + "color": "base-content",
10066 + "dash": "solid",
10067 + "end_gap": 6,
10068 + "from": {
10069 + "_other": "b",
10070 + "anchor": "auto",
10071 + "element": "a"
10072 + },
10073 + "id": "k",
10074 + "marker_end": "arrow",
10075 + "marker_start": "none",
10076 + "routing": "straight",
10077 + "start_gap": 6,
10078 + "stroke_width": 2,
10079 + "to": {
10080 + "_other": "a",
10081 + "anchor": "auto",
10082 + "element": "b"
10083 + }
10084 + },
10085 + "document": "overlapping",
10086 + "expected": {
10087 + "d": "M 106 50 L 14 70",
10088 + "dash_array": null,
10089 + "marker_end": "arrow",
10090 + "marker_start": "none",
10091 + "stroke_width": 2
10092 + },
10093 + "name": "overlapping/straight/auto->auto"
10094 + },
10095 + {
10096 + "connector": {
10097 + "color": "base-content",
10098 + "dash": "solid",
10099 + "end_gap": 6,
10100 + "from": {
10101 + "_other": "b",
10102 + "anchor": "auto",
10103 + "element": "a"
10104 + },
10105 + "id": "k",
10106 + "marker_end": "arrow",
10107 + "marker_start": "none",
10108 + "routing": "straight",
10109 + "start_gap": 6,
10110 + "stroke_width": 2,
10111 + "to": {
10112 + "anchor": "w",
10113 + "element": "b"
10114 + }
10115 + },
10116 + "document": "overlapping",
10117 + "expected": {
10118 + "d": "M 106 50 L 14 70",
10119 + "dash_array": null,
10120 + "marker_end": "arrow",
10121 + "marker_start": "none",
10122 + "stroke_width": 2
10123 + },
10124 + "name": "overlapping/straight/auto->w"
10125 + },
10126 + {
10127 + "connector": {
10128 + "color": "base-content",
10129 + "dash": "solid",
10130 + "end_gap": 6,
10131 + "from": {
10132 + "anchor": "n",
10133 + "element": "a"
10134 + },
10135 + "id": "k",
10136 + "marker_end": "arrow",
10137 + "marker_start": "none",
10138 + "routing": "straight",
10139 + "start_gap": 6,
10140 + "stroke_width": 2,
10141 + "to": {
10142 + "_other": "a",
10143 + "anchor": "auto",
10144 + "element": "b"
10145 + }
10146 + },
10147 + "document": "overlapping",
10148 + "expected": {
10149 + "d": "M 50 -6 L 14 70",
8890 10150 "dash_array": null,
8891 10151 "marker_end": "arrow",
8892 10152 "marker_start": "none",
8893 10153 "stroke_width": 2
8894 10154 },
8895 − "name": "overlapping/straight/point->e"
10155 + "name": "overlapping/straight/n->auto"
8896 10156 },
8897 10157 {
8898 10158 "connector": {
@@ -8900,7 +10160,8 @@
8900 10160 "dash": "solid",
8901 10161 "end_gap": 6,
8902 10162 "from": {
8903 − "anchor": "e",
10163 + "_other": "ghost",
10164 + "anchor": "auto",
8904 10165 "element": "a"
8905 10166 },
8906 10167 "id": "k",
@@ -8910,21 +10171,20 @@
8910 10171 "start_gap": 6,
8911 10172 "stroke_width": 2,
8912 10173 "to": {
8913 − "anchor": "point",
8914 − "element": "b",
8915 − "x": 400,
8916 − "y": 333.75
10174 + "_other": "a",
10175 + "anchor": "auto",
10176 + "element": "b"
8917 10177 }
8918 10178 },
8919 10179 "document": "overlapping",
8920 10180 "expected": {
8921 − "d": "M 106 50 L 400 333.75",
10181 + "d": "M 106 50 L 14 70",
8922 10182 "dash_array": null,
8923 10183 "marker_end": "arrow",
8924 10184 "marker_start": "none",
8925 10185 "stroke_width": 2
8926 10186 },
8927 − "name": "overlapping/straight/e->point"
10187 + "name": "overlapping/straight/auto->auto"
8928 10188 },
8929 10189 {
8930 10190 "connector": {
@@ -9740,6 +11000,132 @@
9740 11000 },
9741 11001 "name": "overlapping/orthogonal/e->point"
9742 11002 },
11003 + {
11004 + "connector": {
11005 + "color": "base-content",
11006 + "dash": "solid",
11007 + "end_gap": 6,
11008 + "from": {
11009 + "_other": "b",
11010 + "anchor": "auto",
11011 + "element": "a"
11012 + },
11013 + "id": "k",
11014 + "marker_end": "arrow",
11015 + "marker_start": "none",
11016 + "routing": "orthogonal",
11017 + "start_gap": 6,
11018 + "stroke_width": 2,
11019 + "to": {
11020 + "_other": "a",
11021 + "anchor": "auto",
11022 + "element": "b"
11023 + }
11024 + },
11025 + "document": "overlapping",
11026 + "expected": {
11027 + "d": "M 106 50 L 60 50 L 60 70 L 14 70",
11028 + "dash_array": null,
11029 + "marker_end": "arrow",
11030 + "marker_start": "none",
11031 + "stroke_width": 2
11032 + },
11033 + "name": "overlapping/orthogonal/auto->auto"
11034 + },
11035 + {
11036 + "connector": {
11037 + "color": "base-content",
11038 + "dash": "solid",
11039 + "end_gap": 6,
11040 + "from": {
11041 + "_other": "b",
11042 + "anchor": "auto",
11043 + "element": "a"
11044 + },
11045 + "id": "k",
11046 + "marker_end": "arrow",
11047 + "marker_start": "none",
11048 + "routing": "orthogonal",
11049 + "start_gap": 6,
11050 + "stroke_width": 2,
11051 + "to": {
11052 + "anchor": "w",
11053 + "element": "b"
11054 + }
11055 + },
11056 + "document": "overlapping",
11057 + "expected": {
11058 + "d": "M 106 50 L 60 50 L 60 70 L 14 70",
11059 + "dash_array": null,
11060 + "marker_end": "arrow",
11061 + "marker_start": "none",
11062 + "stroke_width": 2
11063 + },
11064 + "name": "overlapping/orthogonal/auto->w"
11065 + },
11066 + {
11067 + "connector": {
11068 + "color": "base-content",
11069 + "dash": "solid",
11070 + "end_gap": 6,
11071 + "from": {
11072 + "anchor": "n",
11073 + "element": "a"
11074 + },
11075 + "id": "k",
11076 + "marker_end": "arrow",
11077 + "marker_start": "none",
11078 + "routing": "orthogonal",
11079 + "start_gap": 6,
11080 + "stroke_width": 2,
11081 + "to": {
11082 + "_other": "a",
11083 + "anchor": "auto",
11084 + "element": "b"
11085 + }
11086 + },
11087 + "document": "overlapping",
11088 + "expected": {
11089 + "d": "M 50 -6 L 50 32 L 14 32 L 14 70",
11090 + "dash_array": null,
11091 + "marker_end": "arrow",
11092 + "marker_start": "none",
11093 + "stroke_width": 2
11094 + },
11095 + "name": "overlapping/orthogonal/n->auto"
11096 + },
11097 + {
11098 + "connector": {
11099 + "color": "base-content",
11100 + "dash": "solid",
11101 + "end_gap": 6,
11102 + "from": {
11103 + "_other": "ghost",
11104 + "anchor": "auto",
11105 + "element": "a"
11106 + },
11107 + "id": "k",
11108 + "marker_end": "arrow",
11109 + "marker_start": "none",
11110 + "routing": "orthogonal",
11111 + "start_gap": 6,
11112 + "stroke_width": 2,
11113 + "to": {
11114 + "_other": "a",
11115 + "anchor": "auto",
11116 + "element": "b"
11117 + }
11118 + },
11119 + "document": "overlapping",
11120 + "expected": {
11121 + "d": "M 106 50 L 60 50 L 60 70 L 14 70",
11122 + "dash_array": null,
11123 + "marker_end": "arrow",
11124 + "marker_start": "none",
11125 + "stroke_width": 2
11126 + },
11127 + "name": "overlapping/orthogonal/auto->auto"
11128 + },
9743 11129 {
9744 11130 "connector": {
9745 11131 "color": "base-content",
@@ -10554,6 +11940,132 @@
10554 11940 },
10555 11941 "name": "overlapping/curve/e->point"
10556 11942 },
11943 + {
11944 + "connector": {
11945 + "color": "base-content",
11946 + "dash": "solid",
11947 + "end_gap": 6,
11948 + "from": {
11949 + "_other": "b",
11950 + "anchor": "auto",
11951 + "element": "a"
11952 + },
11953 + "id": "k",
11954 + "marker_end": "arrow",
11955 + "marker_start": "none",
11956 + "routing": "curve",
11957 + "start_gap": 6,
11958 + "stroke_width": 2,
11959 + "to": {
11960 + "_other": "a",
11961 + "anchor": "auto",
11962 + "element": "b"
11963 + }
11964 + },
11965 + "document": "overlapping",
11966 + "expected": {
11967 + "d": "M 106 50 C 143.66 50, -23.66 70, 14 70",
11968 + "dash_array": null,
11969 + "marker_end": "arrow",
11970 + "marker_start": "none",
11971 + "stroke_width": 2
11972 + },
11973 + "name": "overlapping/curve/auto->auto"
11974 + },
11975 + {
11976 + "connector": {
11977 + "color": "base-content",
11978 + "dash": "solid",
11979 + "end_gap": 6,
11980 + "from": {
11981 + "_other": "b",
11982 + "anchor": "auto",
11983 + "element": "a"
11984 + },
11985 + "id": "k",
11986 + "marker_end": "arrow",
11987 + "marker_start": "none",
11988 + "routing": "curve",
11989 + "start_gap": 6,
11990 + "stroke_width": 2,
11991 + "to": {
11992 + "anchor": "w",
11993 + "element": "b"
11994 + }
11995 + },
11996 + "document": "overlapping",
11997 + "expected": {
11998 + "d": "M 106 50 C 143.66 50, -23.66 70, 14 70",
11999 + "dash_array": null,
12000 + "marker_end": "arrow",
12001 + "marker_start": "none",
12002 + "stroke_width": 2
12003 + },
12004 + "name": "overlapping/curve/auto->w"
12005 + },
12006 + {
12007 + "connector": {
12008 + "color": "base-content",
12009 + "dash": "solid",
12010 + "end_gap": 6,
12011 + "from": {
12012 + "anchor": "n",
12013 + "element": "a"
12014 + },
12015 + "id": "k",
12016 + "marker_end": "arrow",
12017 + "marker_start": "none",
12018 + "routing": "curve",
12019 + "start_gap": 6,
12020 + "stroke_width": 2,
12021 + "to": {
12022 + "_other": "a",
12023 + "anchor": "auto",
12024 + "element": "b"
12025 + }
12026 + },
12027 + "document": "overlapping",
12028 + "expected": {
12029 + "d": "M 50 -6 C 50 -39.64, -19.64 70, 14 70",
12030 + "dash_array": null,
12031 + "marker_end": "arrow",
12032 + "marker_start": "none",
12033 + "stroke_width": 2
12034 + },
12035 + "name": "overlapping/curve/n->auto"
12036 + },
12037 + {
12038 + "connector": {
12039 + "color": "base-content",
12040 + "dash": "solid",
12041 + "end_gap": 6,
12042 + "from": {
12043 + "_other": "ghost",
12044 + "anchor": "auto",
12045 + "element": "a"
12046 + },
12047 + "id": "k",
12048 + "marker_end": "arrow",
12049 + "marker_start": "none",
12050 + "routing": "curve",
12051 + "start_gap": 6,
12052 + "stroke_width": 2,
12053 + "to": {
12054 + "_other": "a",
12055 + "anchor": "auto",
12056 + "element": "b"
12057 + }
12058 + },
12059 + "document": "overlapping",
12060 + "expected": {
12061 + "d": "M 106 50 C 143.66 50, -23.66 70, 14 70",
12062 + "dash_array": null,
12063 + "marker_end": "arrow",
12064 + "marker_start": "none",
12065 + "stroke_width": 2
12066 + },
12067 + "name": "overlapping/curve/auto->auto"
12068 + },
10557 12069 {
10558 12070 "connector": {
10559 12071 "color": "base-content",
modified docs/android.md
+21 −21
@@ -540,6 +540,10 @@ document parser and viewport maths. **Not verified:** the drawing itself
540 540 a diagram on a screen yet. Worth doing before phase 4 puts edits behind
541 541 it.
542 542
543 +The `auto`-anchor behaviour described in section 9 was found the hard
544 +way after this phase shipped: connectors drew to the wrong edge on the
545 +phone until the port learned to read `_other`.
546 +
543 547 Number formatting was the subtle part of the port. Erlang's
544 548 `float_to_binary(n, [:compact, decimals: 2])` prints 1.999 as `"2.0"`,
545 549 not `"2"`, while a genuinely whole value prints as `"30"` — so the Kotlin
@@ -603,27 +607,23 @@ action that is always refused is worse than no button at all.
603 607
604 608 ## 9. Open questions
605 609
606 −- **`auto` anchors always leave the east edge.** `ConnectorGeometry`
607 − documents `auto` as picking the cardinal edge whose direction best
608 − aligns with the line between the two bounding boxes, and `best_edge/2`
609 − implements exactly that — but the `"_other"` key it reads is never
610 − written by anything in `lib/`. So `other_center/2` returns nil,
611 − `best_edge` compares an element's centre against itself, and the
612 − `abs(dx) >= abs(dy) and dx >= 0` branch wins every time. Two shapes
613 − side by side get a connector out of both east edges, whatever their
614 − relative positions.
615 −
616 − The fix is small — `render/2` has both endpoints, so it can pass each
617 − anchor the *other* endpoint's element id — but it changes how existing
618 − diagrams look on the web, so it is a decision rather than a cleanup.
619 − The Kotlin port reproduces today's behaviour deliberately, and the
620 − fixtures encode it; regenerate them after any fix and the port is held
621 − honest either way.
622 −
623 −- **Text rendering fidelity.** The web renders text elements as SVG/HTML
624 − with daisyUI typography; Compose text metrics will not match exactly.
625 − Wrapping and vertical centering are the likely visible differences. Do we
626 − accept "close", or does the server need to hand back measured line boxes?
610 +- ~~**`auto` anchors always leave the east edge.**~~ Wrong, and worth
611 + recording as a caution. `ConnectorGeometry` resolves an `auto` anchor
612 + against `anchor["_other"]`, and grepping `lib/` for `"_other"` finds
613 + only the read — so it looked like a dead code path with the east edge
614 + as its permanent fallback. The write is in the editor's colocated
615 + JavaScript (`canvas_live/edit.ex`), which stamps each anchor with the
616 + *opposite* endpoint's id as the connector is created, and the quoted
617 + grep pattern missed the JS object key.
618 +
619 + The port ignored `_other` on the strength of that reading, so every
620 + connector on the phone left its shape's east edge while the web routed
621 + to the nearest one. The fixtures agreed with the port because the
622 + generator never set `_other` either — a matrix that only covers the
623 + fallback proves only that both sides fall back the same way. Fixed,
624 + and the matrix now carries the hint, including one pointing at a
625 + deleted element.
626 +
627 627 - **Anchor validation is looser than it looks.** `Document.valid_anchor?/1`
628 628 ends in a catch-all `%{}` clause, so any map-shaped anchor is accepted —
629 629 an unrecognised name like `"northwest"`, or `"point"` with no
modified lib/mix/tasks/diogramos.android_fixtures.ex
+11 −1
@@ -119,7 +119,17 @@ defmodule Mix.Tasks.Diogramos.AndroidFixtures do
119 119
120 120 point_pairs = [
121 121 {%{"anchor" => "point", "x" => 12.5, "y" => -3}, %{"anchor" => "e"}},
122 − {%{"anchor" => "e"}, %{"anchor" => "point", "x" => 400, "y" => 333.75}}
122 + {%{"anchor" => "e"}, %{"anchor" => "point", "x" => 400, "y" => 333.75}},
123 + # What the editor actually stores: the JS that creates a connector
124 + # writes `_other` into each anchor naming the opposite endpoint
125 + # (canvas_live/edit.ex), and that is what makes "auto" pick the
126 + # nearest edge instead of always leaving east. Without these cases
127 + # the matrix only ever exercised the fallback.
128 + {%{"anchor" => "auto", "_other" => "b"}, %{"anchor" => "auto", "_other" => "a"}},
129 + {%{"anchor" => "auto", "_other" => "b"}, %{"anchor" => "w"}},
130 + {%{"anchor" => "n"}, %{"anchor" => "auto", "_other" => "a"}},
131 + # A hint pointing at an element that no longer exists falls back.
132 + {%{"anchor" => "auto", "_other" => "ghost"}, %{"anchor" => "auto", "_other" => "a"}}
123 133 ]
124 134
125 135 base =

Parents: b453bb1