From 98a02546c29a34fb5e2fb259695e80cd5a2a9cef Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sun, 2 Mar 2025 10:45:42 -0700 Subject: [PATCH] save --- d2target/d2target.go | 3 + .../c4-person-shape/dagre/board.exp.json | 502 ++++++++++ .../c4-person-shape/dagre/sketch.exp.svg | 859 ++++++++++++++++++ .../txtar/c4-person-shape/elk/board.exp.json | 482 ++++++++++ .../txtar/c4-person-shape/elk/sketch.exp.svg | 859 ++++++++++++++++++ e2etests/txtar.txt | 48 + lib/shape/shape.go | 3 + lib/shape/shape_c4_person.go | 154 ++++ 8 files changed, 2910 insertions(+) create mode 100644 e2etests/testdata/txtar/c4-person-shape/dagre/board.exp.json create mode 100644 e2etests/testdata/txtar/c4-person-shape/dagre/sketch.exp.svg create mode 100644 e2etests/testdata/txtar/c4-person-shape/elk/board.exp.json create mode 100644 e2etests/testdata/txtar/c4-person-shape/elk/sketch.exp.svg create mode 100644 lib/shape/shape_c4_person.go diff --git a/d2target/d2target.go b/d2target/d2target.go index e78a264b7..d8db1c5e9 100644 --- a/d2target/d2target.go +++ b/d2target/d2target.go @@ -933,6 +933,7 @@ const ( ShapeCallout = "callout" ShapeStoredData = "stored_data" ShapePerson = "person" + ShapeC4Person = "c4-person" ShapeDiamond = "diamond" ShapeOval = "oval" ShapeCircle = "circle" @@ -960,6 +961,7 @@ var Shapes = []string{ ShapeCallout, ShapeStoredData, ShapePerson, + ShapeC4Person, ShapeDiamond, ShapeOval, ShapeCircle, @@ -1028,6 +1030,7 @@ var DSL_SHAPE_TO_SHAPE_TYPE = map[string]string{ ShapeCallout: shape.CALLOUT_TYPE, ShapeStoredData: shape.STORED_DATA_TYPE, ShapePerson: shape.PERSON_TYPE, + ShapeC4Person: shape.C4_PERSON_TYPE, ShapeDiamond: shape.DIAMOND_TYPE, ShapeOval: shape.OVAL_TYPE, ShapeCircle: shape.CIRCLE_TYPE, diff --git a/e2etests/testdata/txtar/c4-person-shape/dagre/board.exp.json b/e2etests/testdata/txtar/c4-person-shape/dagre/board.exp.json new file mode 100644 index 000000000..f14fb0556 --- /dev/null +++ b/e2etests/testdata/txtar/c4-person-shape/dagre/board.exp.json @@ -0,0 +1,502 @@ +{ + "name": "", + "config": { + "sketch": false, + "themeID": 0, + "darkThemeID": null, + "pad": null, + "center": null, + "layoutEngine": null + }, + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "c4mdperson", + "type": "c4-person", + "pos": { + "x": 0, + "y": 544 + }, + "width": 312, + "height": 350, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 1, + "borderRadius": 0, + "fill": "#08427b", + "stroke": "black", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "## Personal Banking Customer\n\n[person]\n\nA customer of the bank, with\\\npersonal bank accounts", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "markdown", + "color": "white", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 287, + "labelHeight": 143, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "c4person", + "type": "c4-person", + "pos": { + "x": 244, + "y": 0 + }, + "width": 135, + "height": 152, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 1, + "borderRadius": 0, + "fill": "#08427b", + "stroke": "black", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "C4 Style Person", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "white", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 110, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "regular_person", + "type": "person", + "pos": { + "x": 89, + "y": 339 + }, + "width": 134, + "height": 89, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B3", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Standard Person", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 119, + "labelHeight": 21, + "labelPosition": "OUTSIDE_BOTTOM_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "styling", + "type": "rectangle", + "pos": { + "x": 362, + "y": 293 + }, + "width": 210, + "height": 546, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B4", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "styling", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 78, + "labelHeight": 36, + "labelPosition": "OUTSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "styling.c4styled", + "type": "c4-person", + "pos": { + "x": 425, + "y": 323 + }, + "width": 85, + "height": 121, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 1, + "borderRadius": 0, + "fill": "#08427b", + "stroke": "black", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c4styled", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "white", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 60, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "styling.c4sized", + "type": "c4-person", + "pos": { + "x": 392, + "y": 629 + }, + "width": 150, + "height": 180, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 1, + "borderRadius": 0, + "fill": "#08427b", + "stroke": "black", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Custom Size", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "white", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 87, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + } + ], + "connections": [ + { + "id": "(regular_person -> c4mdperson)[0]", + "src": "regular_person", + "srcArrow": "none", + "dst": "c4mdperson", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 156, + "y": 454 + }, + { + "x": 156, + "y": 486 + }, + { + "x": 156, + "y": 502.79998779296875 + }, + { + "x": 156, + "y": 538 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(c4person -> regular_person)[0]", + "src": "c4person", + "srcArrow": "none", + "dst": "regular_person", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "Compare shapes", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 111, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 245, + "y": 135 + }, + { + "x": 173.8000030517578, + "y": 197 + }, + { + "x": 156, + "y": 286.20001220703125 + }, + { + "x": 156, + "y": 339 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(c4person -> styling.c4styled)[0]", + "src": "c4person", + "srcArrow": "none", + "dst": "styling.c4styled", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 379, + "y": 135 + }, + { + "x": 449.3999938964844, + "y": 197 + }, + { + "x": 467, + "y": 283.6000061035156 + }, + { + "x": 467, + "y": 326 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "styling.(c4styled -> c4sized)[0]", + "src": "styling.c4styled", + "srcArrow": "none", + "dst": "styling.c4sized", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 467, + "y": 445 + }, + { + "x": 467, + "y": 484.20001220703125 + }, + { + "x": 467, + "y": 520.7990112304688 + }, + { + "x": 467, + "y": 628 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ], + "root": { + "id": "", + "type": "", + "pos": { + "x": 0, + "y": 0 + }, + "width": 0, + "height": 0, + "opacity": 0, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "N7", + "stroke": "", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 0 + } +} diff --git a/e2etests/testdata/txtar/c4-person-shape/dagre/sketch.exp.svg b/e2etests/testdata/txtar/c4-person-shape/dagre/sketch.exp.svg new file mode 100644 index 000000000..654d8d6b4 --- /dev/null +++ b/e2etests/testdata/txtar/c4-person-shape/dagre/sketch.exp.svg @@ -0,0 +1,859 @@ +

Personal Banking Customer

+

[person]

+

A customer of the bank, with
+personal bank accounts

+
C4 Style PersonStandard Personstylingc4styledCustom Size Compare shapes + + + + + + + + +
\ No newline at end of file diff --git a/e2etests/testdata/txtar/c4-person-shape/elk/board.exp.json b/e2etests/testdata/txtar/c4-person-shape/elk/board.exp.json new file mode 100644 index 000000000..c9290285f --- /dev/null +++ b/e2etests/testdata/txtar/c4-person-shape/elk/board.exp.json @@ -0,0 +1,482 @@ +{ + "name": "", + "config": { + "sketch": false, + "themeID": 0, + "darkThemeID": null, + "pad": null, + "center": null, + "layoutEngine": null + }, + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "c4mdperson", + "type": "c4-person", + "pos": { + "x": 12, + "y": 975 + }, + "width": 312, + "height": 350, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 1, + "borderRadius": 0, + "fill": "#08427b", + "stroke": "black", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "## Personal Banking Customer\n\n[person]\n\nA customer of the bank, with\\\npersonal bank accounts", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "markdown", + "color": "white", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 287, + "labelHeight": 143, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "c4person", + "type": "c4-person", + "pos": { + "x": 201, + "y": 12 + }, + "width": 135, + "height": 152, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 1, + "borderRadius": 0, + "fill": "#08427b", + "stroke": "black", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "C4 Style Person", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "white", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 110, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "regular_person", + "type": "person", + "pos": { + "x": 101, + "y": 790 + }, + "width": 134, + "height": 89, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B3", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Standard Person", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 119, + "labelHeight": 21, + "labelPosition": "OUTSIDE_BOTTOM_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "styling", + "type": "rectangle", + "pos": { + "x": 244, + "y": 249 + }, + "width": 250, + "height": 471, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B4", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "styling", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 78, + "labelHeight": 36, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "styling.c4styled", + "type": "c4-person", + "pos": { + "x": 327, + "y": 299 + }, + "width": 85, + "height": 121, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 1, + "borderRadius": 0, + "fill": "#08427b", + "stroke": "black", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c4styled", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "white", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 60, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "styling.c4sized", + "type": "c4-person", + "pos": { + "x": 294, + "y": 490 + }, + "width": 150, + "height": 180, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 1, + "borderRadius": 0, + "fill": "#08427b", + "stroke": "black", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Custom Size", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "white", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 87, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + } + ], + "connections": [ + { + "id": "(regular_person -> c4mdperson)[0]", + "src": "regular_person", + "srcArrow": "none", + "dst": "c4mdperson", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 168, + "y": 905 + }, + { + "x": 168, + "y": 969 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(c4person -> regular_person)[0]", + "src": "c4person", + "srcArrow": "none", + "dst": "regular_person", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "Compare shapes", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 111, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 246, + "y": 165 + }, + { + "x": 246.25, + "y": 204 + }, + { + "x": 168, + "y": 204 + }, + { + "x": 168, + "y": 790 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(c4person -> styling.c4styled)[0]", + "src": "c4person", + "srcArrow": "none", + "dst": "styling.c4styled", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 291, + "y": 165 + }, + { + "x": 291.25, + "y": 204 + }, + { + "x": 369.5, + "y": 204 + }, + { + "x": 370, + "y": 302 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "styling.(c4styled -> c4sized)[0]", + "src": "styling.c4styled", + "srcArrow": "none", + "dst": "styling.c4sized", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 369, + "y": 421 + }, + { + "x": 370, + "y": 489 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ], + "root": { + "id": "", + "type": "", + "pos": { + "x": 0, + "y": 0 + }, + "width": 0, + "height": 0, + "opacity": 0, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "N7", + "stroke": "", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 0 + } +} diff --git a/e2etests/testdata/txtar/c4-person-shape/elk/sketch.exp.svg b/e2etests/testdata/txtar/c4-person-shape/elk/sketch.exp.svg new file mode 100644 index 000000000..8eae5ca4c --- /dev/null +++ b/e2etests/testdata/txtar/c4-person-shape/elk/sketch.exp.svg @@ -0,0 +1,859 @@ +

Personal Banking Customer

+

[person]

+

A customer of the bank, with
+personal bank accounts

+
C4 Style PersonStandard Personstylingc4styledCustom Size Compare shapes + + + + + + + + +
\ No newline at end of file diff --git a/e2etests/txtar.txt b/e2etests/txtar.txt index 92fe71d6c..85b1e141e 100644 --- a/e2etests/txtar.txt +++ b/e2etests/txtar.txt @@ -978,3 +978,51 @@ cloud: |md blah blah | + +-- c4-person-shape -- +c4mdperson.shape: c4-person +c4mdperson: |md +## Personal Banking Customer + +[person] + +A customer of the bank, with\ +personal bank accounts + | +c4person: { + shape: c4-person + label: "C4 Style Person" +} +regular_person -> c4mdperson + +regular_person: { + shape: person + label: "Standard Person" +} + +c4person -> regular_person: "Compare shapes" + +styling: { + c4styled: { + shape: c4-person + style.fill: "#91BEEA" + style.stroke: "#2E6195" + style.stroke-width: 2 + } + + c4sized: { + shape: c4-person + width: 150 + height: 180 + label: "Custom Size" + } +} + +c4person -> styling.c4styled -> styling.c4sized +**: { + &shape: c4-person + style.fill: "#08427b" + style.stroke: black + style.font-color: white + style.stroke-width: 1 +} diff --git a/lib/shape/shape.go b/lib/shape/shape.go index 50ce3e324..6e3e0b81b 100644 --- a/lib/shape/shape.go +++ b/lib/shape/shape.go @@ -21,6 +21,7 @@ const ( CALLOUT_TYPE = "Callout" STORED_DATA_TYPE = "StoredData" PERSON_TYPE = "Person" + C4_PERSON_TYPE = "c4-person" DIAMOND_TYPE = "Diamond" OVAL_TYPE = "Oval" CIRCLE_TYPE = "Circle" @@ -155,6 +156,8 @@ func NewShape(shapeType string, box *geo.Box) Shape { return NewParallelogram(box) case PERSON_TYPE: return NewPerson(box) + case C4_PERSON_TYPE: + return NewC4Person(box) case QUEUE_TYPE: return NewQueue(box) case REAL_SQUARE_TYPE: diff --git a/lib/shape/shape_c4_person.go b/lib/shape/shape_c4_person.go new file mode 100644 index 000000000..eb70c2a62 --- /dev/null +++ b/lib/shape/shape_c4_person.go @@ -0,0 +1,154 @@ +package shape + +import ( + "math" + + "oss.terrastruct.com/d2/lib/geo" + "oss.terrastruct.com/d2/lib/svg" + "oss.terrastruct.com/util-go/go2" +) + +// Optimal value for circular arc approximation with cubic bezier curves +const kCircleApprox = 0.5522847498307936 // 4*(math.Sqrt(2)-1)/3 + +type shapeC4Person struct { + *baseShape +} + +func NewC4Person(box *geo.Box) Shape { + shape := shapeC4Person{ + baseShape: &baseShape{ + Type: C4_PERSON_TYPE, + Box: box, + }, + } + shape.FullShape = go2.Pointer(Shape(shape)) + return shape +} + +const ( + C4_PERSON_AR_LIMIT = 1.5 +) + +func (s shapeC4Person) GetInnerBox() *geo.Box { + width := s.Box.Width + height := s.Box.Height + + headRadius := width * 0.22 + headCenterY := height * 0.18 + bodyTop := headCenterY + headRadius*0.8 + + tl := s.Box.TopLeft.Copy() + horizontalPadding := width * 0.1 + tl.X += horizontalPadding + tl.Y += bodyTop + height*0.05 + + innerWidth := width - (horizontalPadding * 2) + innerHeight := height - tl.Y + s.Box.TopLeft.Y - (height * 0.05) + + return geo.NewBox(tl, innerWidth, innerHeight) +} + +func bodyPath(box *geo.Box) *svg.SvgPathContext { + width := box.Width + height := box.Height + + pc := svg.NewSVGPathContext(box.TopLeft, 1, 1) + + headRadius := width * 0.22 + headCenterY := height * 0.18 + bodyTop := headCenterY + headRadius*0.8 + bodyWidth := width + bodyHeight := height - bodyTop + bodyLeft := 0 + cornerRadius := width * 0.175 + + pc.StartAt(pc.Absolute(float64(bodyLeft), bodyTop+cornerRadius)) + + pc.C(true, 0, -kCircleApprox*cornerRadius, kCircleApprox*cornerRadius, -cornerRadius, cornerRadius, -cornerRadius) + pc.H(true, bodyWidth-2*cornerRadius) + pc.C(true, kCircleApprox*cornerRadius, 0, cornerRadius, kCircleApprox*cornerRadius, cornerRadius, cornerRadius) + pc.V(true, bodyHeight-2*cornerRadius) + pc.C(true, 0, kCircleApprox*cornerRadius, -kCircleApprox*cornerRadius, cornerRadius, -cornerRadius, cornerRadius) + pc.H(true, -(bodyWidth - 2*cornerRadius)) + pc.C(true, -kCircleApprox*cornerRadius, 0, -cornerRadius, -kCircleApprox*cornerRadius, -cornerRadius, -cornerRadius) + pc.Z() + + return pc +} + +func headPath(box *geo.Box) *svg.SvgPathContext { + width := box.Width + height := box.Height + + pc := svg.NewSVGPathContext(box.TopLeft, 1, 1) + + headRadius := width * 0.22 + headCenterX := width / 2 + headCenterY := height * 0.18 + + pc.StartAt(pc.Absolute(headCenterX, headCenterY-headRadius)) + + pc.C(false, + headCenterX+headRadius*kCircleApprox, headCenterY-headRadius, + headCenterX+headRadius, headCenterY-headRadius*kCircleApprox, + headCenterX+headRadius, headCenterY) + + pc.C(false, + headCenterX+headRadius, headCenterY+headRadius*kCircleApprox, + headCenterX+headRadius*kCircleApprox, headCenterY+headRadius, + headCenterX, headCenterY+headRadius) + + pc.C(false, + headCenterX-headRadius*kCircleApprox, headCenterY+headRadius, + headCenterX-headRadius, headCenterY+headRadius*kCircleApprox, + headCenterX-headRadius, headCenterY) + + pc.C(false, + headCenterX-headRadius, headCenterY-headRadius*kCircleApprox, + headCenterX-headRadius*kCircleApprox, headCenterY-headRadius, + headCenterX, headCenterY-headRadius) + + return pc +} + +func (s shapeC4Person) Perimeter() []geo.Intersectable { + width := s.Box.Width + height := s.Box.Height + + bodyPerimeter := bodyPath(s.Box).Path + + headRadius := width * 0.22 + headCenterX := s.Box.TopLeft.X + width/2 + headCenterY := s.Box.TopLeft.Y + height*0.18 + headCenter := geo.NewPoint(headCenterX, headCenterY) + + headEllipse := geo.NewEllipse(headCenter, headRadius, headRadius) + + return append(bodyPerimeter, headEllipse) +} + +func (s shapeC4Person) GetSVGPathData() []string { + return []string{ + bodyPath(s.Box).PathData(), + headPath(s.Box).PathData(), + } +} + +func (s shapeC4Person) GetDimensionsToFit(width, height, paddingX, paddingY float64) (float64, float64) { + totalWidth := width + paddingX + totalHeight := height + paddingY + + if totalHeight < totalWidth*0.8 { + totalHeight = totalWidth * 0.8 + } + + totalHeight *= 1.4 + + totalWidth, totalHeight = LimitAR(totalWidth, totalHeight, C4_PERSON_AR_LIMIT) + return math.Ceil(totalWidth), math.Ceil(totalHeight) +} + +func (s shapeC4Person) GetDefaultPadding() (paddingX, paddingY float64) { + return 20, defaultPadding * 1.5 +}