mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-13 02:32:09 +10:00
215 lines
11 KiB
Markdown
215 lines
11 KiB
Markdown
# Orthogonal Connector
|
|
|
|
A wire that runs only horizontally and vertically, turning at rounded 90-degree
|
|
corners.
|
|
|
|
## 1. Goal
|
|
|
|
`HookupWire` draws a free Bezier curve between two sticky endpoints;
|
|
`Line` draws a single straight segment. Neither can express the routing style
|
|
used in schematics and block diagrams, where a connection leaves a terminal on
|
|
an axis, turns at right angles, and arrives on an axis. This document designs
|
|
that third kind of connector.
|
|
|
|
The shapes that must be reachable are, at minimum, horizontal-vertical-horizontal
|
|
and vertical-horizontal-vertical. Simple L bends and longer staircases should
|
|
fall out of the same mechanism rather than needing a separate component.
|
|
|
|
## 2. How many handles does the route actually need
|
|
|
|
An orthogonal route is a polyline whose segments alternate between horizontal
|
|
and vertical. Both endpoints are pinned, so counting the free numbers is easy:
|
|
a route of `s` segments has `s - 1` corners and `s - 2` continuous degrees of
|
|
freedom, plus **one discrete bit** saying whether the first segment leaves the
|
|
start point horizontally or vertically.
|
|
|
|
| Segments | Shape | Corners | Continuous DOF | Bit |
|
|
| --- | --- | --- | --- | --- |
|
|
| 2 | `HV` / `VH` (an L) | 1 | 0 | yes |
|
|
| 3 | `HVH` / `VHV` | 2 | 1 | yes |
|
|
| 4 | `HVHV` / `VHVH` | 3 | 2 | yes |
|
|
| 6 | `HVHVHV` / … | 5 | 4 | yes |
|
|
|
|
This is the crux of the problem stated in the request. A single interior control
|
|
point carries two numbers, but the `HVH` / `VHV` case needs *one* number and
|
|
*one bit*. One point is simultaneously one coordinate too many and one bit too
|
|
few, so no scheme that reads only that point's position can resolve the
|
|
arrangement without a heuristic that flips as the user drags across a diagonal.
|
|
|
|
The line in the table that is exactly parameterised is the four-segment one: three
|
|
corners, two degrees of freedom, and a middle corner that is a genuine free point
|
|
in the plane. That observation drives the whole design.
|
|
|
|
## 3. The chosen parameterisation
|
|
|
|
**Each interior control point is a real bend vertex, and the route between
|
|
consecutive vertices is a fixed `H` then `V` pair.** For one interior point `h`
|
|
the route is
|
|
|
|
```
|
|
A ────────────┐ A = start (sticky)
|
|
│ c1 = (h.x, A.y) (derived)
|
|
h ─────────┐ h = the handle (draggable)
|
|
│ c2 = (B.x, h.y) (derived)
|
|
B B = end (sticky)
|
|
```
|
|
|
|
that is `A → (h.x, A.y) → h → (B.x, h.y) → B`: four segments, alternating,
|
|
starting horizontal and ending vertical. Both of the handle's coordinates are
|
|
used, nothing is discarded, and there is no hidden state.
|
|
|
|
The requested arrangements are reached by letting segments collapse to zero
|
|
length, which is what snap-to-grid makes trivial:
|
|
|
|
| Handle placed at | Route degenerates to |
|
|
| --- | --- |
|
|
| anywhere | `HVHV`, three visible bends |
|
|
| `h.y == B.y` | `HVH`, two bends, vertical leg at `h.x` |
|
|
| `h.x == A.x` | `VHV`, two bends, horizontal leg at `h.y` |
|
|
| `h == (A.x, B.y)` | `VH`, one bend |
|
|
| `h == (B.x, A.y)` | `HV`, one bend |
|
|
|
|
The handle may sit outside the box spanned by the endpoints, which gives routes
|
|
that detour around obstacles. Nothing special is needed for that case.
|
|
|
|
### Generalising
|
|
|
|
`k` interior points chain the same template:
|
|
|
|
```
|
|
A → (h1.x, A.y) → h1 → (h2.x, h1.y) → h2 → … → (B.x, hk.y) → B
|
|
```
|
|
|
|
This is `2k + 2` segments and `2k` degrees of freedom, matching the `s - 2` rule
|
|
exactly. Every additional handle buys two more bends, every stored number is
|
|
meaningful, and the normalisation code is the same loop regardless of `k`. The
|
|
number of handles is an enum property, mirroring `AbstractCurvedComponent`'s
|
|
`Point Count`.
|
|
|
|
### The bit does not need a property
|
|
|
|
The template above always leaves the start horizontally, so in principle there is
|
|
a bit to expose. In practice there is not, because the collapses in the table
|
|
already cover every way a route can meet its endpoints:
|
|
|
|
| Leaves the start | Arrives at the end | Reached by |
|
|
| --- | --- | --- |
|
|
| horizontally | vertically | bend point anywhere |
|
|
| horizontally | horizontally | bend point on the end row |
|
|
| vertically | vertically | bend point on the start column |
|
|
| vertically | horizontally | bend point on the opposite corner of the box |
|
|
|
|
A `Start Direction` property would therefore be a second, less discoverable way
|
|
of doing what dragging already does, so it is not offered. The single shape it
|
|
would add is the mirror-image staircase through a given bend point, which one
|
|
more bend point reaches anyway.
|
|
|
|
The bit does survive as a plain field, because a quarter-turn rotation swaps the
|
|
two axes and an `H V H V` route becomes a `V H V H` one, which the fixed template
|
|
cannot express. `OrthogonalComponentTransformer` records the swap there so that
|
|
rotating a component yields exactly the rotated route. Nothing else reads or
|
|
writes it, and it never appears in the property editor.
|
|
|
|
### Why not the alternatives
|
|
|
|
| Scheme | Verdict |
|
|
| --- | --- |
|
|
| One handle, only one coordinate honoured, arrangement from an enum | Half of the handle's motion is visibly inert with no explanation, and it does not generalise past two bends without a second property. |
|
|
| One handle, arrangement inferred from which side of the diagonal it is on | The route snaps between `HVH` and `VHV` mid-drag. Predictable to implement, unpleasant to use. |
|
|
| Store corners raw and project them onto a legal path on every read | Over-parameterised: `2k` stored numbers for `k - 1` degrees of freedom, so the projection needs to know which point moved last, and a whole-component drag rewrites the raw values and loses the arrangement. |
|
|
|
|
## 4. Class layout
|
|
|
|
| Class | Module | Role |
|
|
| --- | --- | --- |
|
|
| `AbstractOrthogonalComponent<T>` | `diylc-core`, `org.diylc.components` | Sibling of `AbstractCurvedComponent`. Owns the control points, the point-count and start-direction properties, the corner radius, path construction, length and caching bounds. Declares `protected abstract void drawCurve(Path2D, Graphics2D, ComponentState, IDrawingObserver)`, the same hook `AbstractCurvedComponent` uses, so concrete subclasses stay thin. |
|
|
| `OrthogonalComponentTransformer` | `diylc-core`, `org.diylc.components.transform` | Delegates point rotation to `SimpleComponentTransformer`, then flips `Start Direction`. Mirroring needs no flip and passes straight through. |
|
|
| `OrthogonalWire` | `diylc-library`, `org.diylc.components.connectivity` | The layout part, shown as `Right Angle Wire` under `Connectivity`. `AWG` value, stripe support, `IContinuity`. Its body is close to a copy of `HookupWire`'s, which is the point of reusing the `drawCurve` hook. |
|
|
| `OrthogonalLine` | `diylc-library`, `org.diylc.components.connectivity` | The schematic part, shown as `Right Angle Line` under `Schematic Symbols`. Draws the same route as a plain stroke of a given thickness, one pixel by default, with the optional arrow heads `Line` offers. |
|
|
|
|
The two concrete parts differ only in `drawCurve` and in the properties each adds
|
|
on top of the base. `Right Angle Line` mirrors `Line`'s property set exactly:
|
|
thickness, colour, style, arrow size and the two arrow toggles. It also carries
|
|
`IContinuity`, which `Line` does not, because it sits in `Schematic Symbols`
|
|
rather than `Shapes` and a wire drawn between two symbols is expected to show up
|
|
in the netlist.
|
|
|
|
Its arrow heads take their direction from the first and the last route segment
|
|
rather than from the straight line between the endpoints, and they are not
|
|
trimmed out of the stroke the way `Line` trims its own. The head is filled in the
|
|
line colour with its tip on the endpoint, so whatever runs underneath it is
|
|
already hidden.
|
|
|
|
## 5. Geometry
|
|
|
|
`buildVertices()` returns the full vertex list including any zero-length
|
|
collapses, then consecutive duplicates are dropped. `buildPath()` walks the
|
|
deduplicated list and rounds each corner:
|
|
|
|
```
|
|
r_i = min(cornerRadius, len(v[i-1], v[i]) / 2, len(v[i], v[i+1]) / 2)
|
|
```
|
|
|
|
The corner is drawn as a quadratic Bezier whose control point is the corner
|
|
itself, from the point `r_i` before it to the point `r_i` after it. Clamping to
|
|
half of each adjacent segment means short segments and collapsed segments
|
|
degrade to sharp corners instead of overshooting; an arc would look marginally
|
|
better at large radii but the difference is invisible at the radii this
|
|
component is for.
|
|
|
|
`Corner Radius` is a `Size` property, default around `0.05in`, `0` giving hard
|
|
corners.
|
|
|
|
## 6. Component contract
|
|
|
|
| Member | Behaviour |
|
|
| --- | --- |
|
|
| `creationMethod` | `SINGLE_CLICK`, as `HookupWire`. `POINT_BY_POINT` only ever assigns control points `0` and `1`, so it cannot place a component whose second endpoint is not index 1. |
|
|
| `isControlPointSticky` | `true` only for the first and last index. |
|
|
| `canControlPointOverlap` | `true` for interior indices. |
|
|
| `getControlPointVisibilityPolicy` | `WHEN_SELECTED` for every index. |
|
|
| `getControlPointNodeName` | `null` for interior indices. |
|
|
| `arePointsConnected` | `true` only for the pair `{0, count - 1}`, exactly as `HookupWire`. |
|
|
| Selection guidelines | Draw the dashed guideline polyline through the derived vertices with tracking off, as `AbstractCurvedComponent.draw` does. |
|
|
| Hit area | Falls out of filling the stroked path while the drawing observer is tracking. |
|
|
| `getLength` | Exact sum of segment lengths, so `IHaveLength` is honest here in a way the Bezier approximation is not. |
|
|
| `enableCache` | `false`, matching `HookupWire`. |
|
|
|
|
## 7. Defaults
|
|
|
|
Dropped on an empty canvas the component spans `DEFAULT_SIZE` diagonally, with
|
|
the single handle at `((A.x + B.x) / 2, B.y)`, which renders as a plain `HVH`
|
|
route. That is the shape most users expect from a right-angle connector, and one
|
|
drag of the handle reaches everything else.
|
|
|
|
## 8. What was built
|
|
|
|
| File | Module |
|
|
| --- | --- |
|
|
| `org/diylc/components/AbstractOrthogonalComponent.java` | `diylc-core` |
|
|
| `org/diylc/components/transform/OrthogonalComponentTransformer.java` | `diylc-core` |
|
|
| `org/diylc/components/connectivity/OrthogonalWire.java` | `diylc-library` |
|
|
| `org/diylc/components/connectivity/OrthogonalLine.java` | `diylc-library` |
|
|
| `org/diylc/components/AbstractOrthogonalComponentTest.java` | `diylc-core`, tests |
|
|
|
|
The three open questions were settled as follows.
|
|
|
|
1. The component is called `Right Angle Wire` and sits in `Connectivity`, next to
|
|
`Hookup Wire`, whose drawing code it shares through the `drawCurve` hook. Its
|
|
property list differs from `Hookup Wire`'s only in carrying `Corner Radius`
|
|
where the curved wire carries `Smooth`.
|
|
2. The handle count ships configurable from the start, as `Point Count` with
|
|
values `Three`, `Four` and `Five`, matching `AbstractCurvedComponent`'s
|
|
property of the same name. Changing it spreads the bend points into an even
|
|
staircase between the two ends.
|
|
3. No snapping was added. The clean shapes are already one grid step away, and a
|
|
snap that pulls a handle onto the start column or end row would fight
|
|
snap-to-grid rather than complement it. This is the decision most worth
|
|
revisiting, since with no `Start Direction` property those two alignments are
|
|
now the only route to the two-bend shapes.
|
|
|
|
The test covers the routing template in both start directions, the degenerate
|
|
shapes, the invariant that every produced segment is axis aligned, point-count
|
|
changes, and the transformer contract that rotating the component yields exactly
|
|
the rotated route.
|