Skip to content

Commit e108b79

Browse files
fix(library): size class-diagram diamonds to match the inheritance triangle (#805)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9c4782c commit e108b79

3 files changed

Lines changed: 73 additions & 5 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tumaet/apollon": patch
3+
---
4+
5+
Aggregation and composition diamonds in class diagrams are now large enough to read at a glance. They were drawn with only about 70% of the ink of the inheritance triangle next to them, so the filled/hollow distinction was easy to miss at normal zoom. The diamond now carries at least the triangle's visual weight — matching how draw.io and Mermaid proportion the two — while staying no taller, so it never overhangs a class box further than the triangle already did.

library/lib/constants.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,14 @@ export const INTERFACE = Object.freeze({
219219
// (used a few lines below for BPMN message markers).
220220
export const MARKER_BASE_SIZE = 18
221221
const BPMN_MARKER_SIZE = 11
222+
// Aggregation/composition diamonds run longer than the other class markers so
223+
// they carry at least the inheritance triangle's visual weight, as in draw.io
224+
// (24-long diamond vs 16-long triangle) and Mermaid (equal areas). Capped at 24
225+
// because 24 * RHOMBUS_HEIGHT_FACTOR stays under the triangle's height, so the
226+
// diamond never overhangs a node border further than the triangle does.
227+
const RHOMBUS_MARKER_SIZE = 24
228+
// 1/phi, inside the 0.588-0.706 thickness band those tools use.
229+
const RHOMBUS_HEIGHT_FACTOR = 0.618
222230

223231
export const EDGES = Object.freeze({
224232
/** Negative padding extends target point to node boundary (React Flow handles are offset 3px) */
@@ -324,20 +332,20 @@ export interface MarkerConfig {
324332
const INTERFACE_SOCKET_SIZE = INTERFACE_RADIUS // Must equal INTERFACE.SIZE / 2
325333

326334
export const MARKER_CONFIGS = Object.freeze({
327-
// Class diagram markers - golden ratio inspired proportions
335+
// Class diagram markers
328336
"black-rhombus": {
329337
type: "rhombus",
330338
filled: true,
331-
size: MARKER_BASE_SIZE,
339+
size: RHOMBUS_MARKER_SIZE,
332340
widthFactor: 1.0,
333-
heightFactor: 0.618,
341+
heightFactor: RHOMBUS_HEIGHT_FACTOR,
334342
},
335343
"white-rhombus": {
336344
type: "rhombus",
337345
filled: false,
338-
size: MARKER_BASE_SIZE,
346+
size: RHOMBUS_MARKER_SIZE,
339347
widthFactor: 1.0,
340-
heightFactor: 0.618,
348+
heightFactor: RHOMBUS_HEIGHT_FACTOR,
341349
},
342350
"white-triangle": {
343351
type: "triangle",
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, expect, it } from "vitest"
2+
import { getMarkerHalfHeight } from "@/components/svgs/edges"
3+
import { MARKER_CONFIGS } from "@/constants"
4+
5+
// The aggregation/composition diamond and the inheritance triangle sit side by
6+
// side in a class diagram, so what a reader perceives is their proportions, not
7+
// their absolute sizes. These pin the relationships that set the diamond's size.
8+
9+
// Both shapes are inscribed in their bounding box, so width * height / 2 is
10+
// their exact area — a proxy for visual weight.
11+
const inkArea = (id: keyof typeof MARKER_CONFIGS) => {
12+
const { size, widthFactor, heightFactor } = MARKER_CONFIGS[id]
13+
return (size * widthFactor * (size * heightFactor)) / 2
14+
}
15+
16+
const height = (id: keyof typeof MARKER_CONFIGS) => getMarkerHalfHeight(id) * 2
17+
18+
describe("class diagram marker geometry", () => {
19+
it("draws both diamonds identically apart from the fill", () => {
20+
const { filled: _b, ...black } = MARKER_CONFIGS["black-rhombus"]
21+
const { filled: _w, ...white } = MARKER_CONFIGS["white-rhombus"]
22+
expect(black).toEqual(white)
23+
expect(MARKER_CONFIGS["black-rhombus"].filled).toBe(true)
24+
expect(MARKER_CONFIGS["white-rhombus"].filled).toBe(false)
25+
})
26+
27+
it("gives the diamond at least the inheritance triangle's visual weight", () => {
28+
// draw.io puts the diamond at ~1.32x the triangle's area, Mermaid at 1.0x.
29+
expect(inkArea("black-rhombus")).toBeGreaterThanOrEqual(
30+
inkArea("white-triangle")
31+
)
32+
})
33+
34+
it("keeps the diamond no taller than the inheritance triangle", () => {
35+
// Height is the extent perpendicular to the edge: how far a marker overhangs
36+
// the node border it points at.
37+
expect(height("black-rhombus")).toBeLessThanOrEqual(
38+
height("white-triangle")
39+
)
40+
})
41+
42+
it("keeps the diamond's thickness in the band used by reference tools", () => {
43+
// draw.io 0.588 (diamondThin), PlantUML 0.667, Mermaid 0.706.
44+
const { widthFactor, heightFactor } = MARKER_CONFIGS["black-rhombus"]
45+
const aspect = heightFactor / widthFactor
46+
expect(aspect).toBeGreaterThanOrEqual(0.588)
47+
expect(aspect).toBeLessThanOrEqual(0.706)
48+
})
49+
50+
it("keeps the diamond small enough to render unscaled in the edge-type dropdown", () => {
51+
// EdgeTypePreviewIcon shrinks markers whose half-height exceeds 11; only the
52+
// node-scale interface socket should need that.
53+
expect(getMarkerHalfHeight("black-rhombus")).toBeLessThanOrEqual(11)
54+
})
55+
})

0 commit comments

Comments
 (0)