@likec4/core 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -45,7 +45,7 @@ export declare const ElementColors: {
45
45
  readonly fill: "#AC4D39";
46
46
  readonly stroke: "#853A2D";
47
47
  readonly hiContrast: "#FBD3CB";
48
- readonly loContrast: "#FF977D";
48
+ readonly loContrast: "#f5b2a3";
49
49
  };
50
50
  readonly green: {
51
51
  readonly fill: "#428a4f";
@@ -48,7 +48,7 @@ export const ElementColors = {
48
48
  hiContrast: '#FBD3CB',
49
49
  // hiContrast: '#f8fafc',
50
50
  // loContrast: '#fdd8d8' // radix black red 12
51
- loContrast: '#FF977D'
51
+ loContrast: '#f5b2a3'
52
52
  },
53
53
  green: {
54
54
  fill: '#428a4f',
@@ -48,7 +48,7 @@ export declare const defaultTheme: {
48
48
  readonly fill: "#AC4D39";
49
49
  readonly stroke: "#853A2D";
50
50
  readonly hiContrast: "#FBD3CB";
51
- readonly loContrast: "#FF977D";
51
+ readonly loContrast: "#f5b2a3";
52
52
  };
53
53
  readonly green: {
54
54
  readonly fill: "#428a4f";
@@ -108,7 +108,7 @@ export declare const defaultTheme: {
108
108
  red: {
109
109
  lineColor: "#AC4D39";
110
110
  labelBgColor: "#b91c1c";
111
- labelColor: "#FF977D";
111
+ labelColor: "#f5b2a3";
112
112
  };
113
113
  secondary: {
114
114
  lineColor: "#0ea5e9";
@@ -37,7 +37,7 @@ export declare const RelationshipColors: {
37
37
  red: {
38
38
  lineColor: "#AC4D39";
39
39
  labelBgColor: "#b91c1c";
40
- labelColor: "#FF977D";
40
+ labelColor: "#f5b2a3";
41
41
  };
42
42
  secondary: {
43
43
  lineColor: "#0ea5e9";
@@ -41,7 +41,7 @@ export const RelationshipColors = {
41
41
  red: {
42
42
  lineColor: '#AC4D39',
43
43
  labelBgColor: '#b91c1c',
44
- labelColor: '#FF977D'
44
+ labelColor: '#f5b2a3'
45
45
  },
46
46
  secondary: sky,
47
47
  sky,
@@ -2,7 +2,7 @@ import type { Opaque } from './opaque';
2
2
  export type NonEmptyArray<T> = [T, ...T[]] | [...T[], T];
3
3
  export type IconUrl = Opaque<string, 'IconUrl'>;
4
4
  export type Point = readonly [x: number, y: number];
5
- export interface XYPosition {
5
+ export interface XYPoint {
6
6
  x: number;
7
7
  y: number;
8
8
  }
@@ -1,9 +1,11 @@
1
1
  import type { IconUrl } from './_common';
2
2
  import type { BorderStyle, ElementKind, ElementShape, Fqn, Tag } from './element';
3
+ import type { WhereOperator } from './operators';
3
4
  import type { RelationshipArrowType, RelationshipLineType } from './relation';
4
5
  import type { ThemeColor } from './theme';
5
6
  import type { ViewID } from './view';
6
7
  interface BaseExpr {
8
+ where?: never;
7
9
  element?: never;
8
10
  custom?: never;
9
11
  expanded?: never;
@@ -30,7 +32,7 @@ export interface ExpandedElementExpr extends Omit<BaseExpr, 'expanded'> {
30
32
  export declare function isExpandedElementExpr(expr: Expression): expr is ExpandedElementExpr;
31
33
  export interface CustomElementExpr extends Omit<BaseExpr, 'custom'> {
32
34
  custom: {
33
- element: Fqn;
35
+ expr: ElementExpression | ElementWhereExpr;
34
36
  title?: string;
35
37
  description?: string;
36
38
  technology?: string;
@@ -59,7 +61,15 @@ export interface ElementTagExpr extends Omit<BaseExpr, 'elementTag' | 'isEqual'>
59
61
  export declare function isElementTagExpr(expr: Expression): expr is ElementTagExpr;
60
62
  export type ElementExpression = ElementRefExpr | WildcardExpr | ElementKindExpr | ElementTagExpr | ExpandedElementExpr;
61
63
  export declare function isElement(expr: Expression): expr is ElementExpression;
62
- export type ElementPredicateExpression = ElementExpression | CustomElementExpr;
64
+ export interface ElementWhereExpr extends Omit<BaseExpr, 'where'> {
65
+ where: {
66
+ expr: ElementExpression;
67
+ condition: WhereOperator;
68
+ };
69
+ }
70
+ export declare function isElementWhere(expr: Expression): expr is ElementWhereExpr;
71
+ export type ElementPredicateExpression = ElementExpression | ElementWhereExpr | CustomElementExpr;
72
+ export declare function isElementPredicateExpr(expr: Expression): expr is ElementPredicateExpression;
63
73
  export interface RelationExpr extends Omit<BaseExpr, 'source' | 'target'> {
64
74
  source: ElementExpression;
65
75
  target: ElementExpression;
@@ -78,10 +88,21 @@ export interface OutgoingExpr extends Omit<BaseExpr, 'outgoing'> {
78
88
  outgoing: ElementExpression;
79
89
  }
80
90
  export declare function isOutgoing(expr: Expression): expr is OutgoingExpr;
91
+ export type RelationExpression = RelationExpr | InOutExpr | IncomingExpr | OutgoingExpr;
92
+ export declare function isRelationExpression(expr: Expression): expr is RelationExpression;
93
+ export interface RelationWhereExpr extends Omit<BaseExpr, 'where'> {
94
+ where: {
95
+ expr: RelationExpression;
96
+ condition: WhereOperator;
97
+ };
98
+ }
99
+ export declare function isRelationWhere(expr: Expression): expr is RelationWhereExpr;
81
100
  export interface CustomRelationExpr extends Omit<BaseExpr, 'customRelation'> {
82
101
  customRelation: {
83
- relation: RelationExpr;
102
+ relation: RelationExpression | RelationWhereExpr;
84
103
  title?: string;
104
+ description?: string;
105
+ technology?: string;
85
106
  color?: ThemeColor;
86
107
  line?: RelationshipLineType;
87
108
  head?: RelationshipArrowType;
@@ -89,9 +110,8 @@ export interface CustomRelationExpr extends Omit<BaseExpr, 'customRelation'> {
89
110
  };
90
111
  }
91
112
  export declare function isCustomRelationExpr(expr: Expression): expr is CustomRelationExpr;
92
- export type RelationExpression = RelationExpr | InOutExpr | IncomingExpr | OutgoingExpr;
93
- export type RelationPredicateExpression = RelationExpression | CustomRelationExpr;
94
- export declare function isAnyRelation(expr: Expression): expr is RelationPredicateExpression;
113
+ export type RelationPredicateExpression = RelationExpression | RelationWhereExpr | CustomRelationExpr;
114
+ export declare function isRelationPredicateExpr(expr: Expression): expr is RelationPredicateExpression;
95
115
  export type Expression = ElementPredicateExpression | RelationPredicateExpression;
96
116
  export {};
97
117
  //# sourceMappingURL=expression.d.ts.map
@@ -5,7 +5,7 @@ export function isExpandedElementExpr(expr) {
5
5
  return 'expanded' in expr;
6
6
  }
7
7
  export function isCustomElement(expr) {
8
- return 'custom' in expr;
8
+ return 'custom' in expr && (isElement(expr.custom.expr) || isElementWhere(expr.custom.expr));
9
9
  }
10
10
  export function isWildcard(expr) {
11
11
  return 'wildcard' in expr;
@@ -23,6 +23,12 @@ export function isElement(expr) {
23
23
  || isElementTagExpr(expr)
24
24
  || isExpandedElementExpr(expr);
25
25
  }
26
+ export function isElementWhere(expr) {
27
+ return 'where' in expr && isElement(expr.where.expr);
28
+ }
29
+ export function isElementPredicateExpr(expr) {
30
+ return isElement(expr) || isElementWhere(expr) || isCustomElement(expr);
31
+ }
26
32
  export function isRelation(expr) {
27
33
  return 'source' in expr && 'target' in expr;
28
34
  }
@@ -35,11 +41,17 @@ export function isIncoming(expr) {
35
41
  export function isOutgoing(expr) {
36
42
  return 'outgoing' in expr;
37
43
  }
44
+ export function isRelationExpression(expr) {
45
+ return isRelation(expr) || isInOut(expr) || isIncoming(expr) || isOutgoing(expr);
46
+ }
47
+ export function isRelationWhere(expr) {
48
+ return 'where' in expr && isRelationExpression(expr.where.expr);
49
+ }
38
50
  export function isCustomRelationExpr(expr) {
39
51
  return 'customRelation' in expr;
40
52
  }
41
- export function isAnyRelation(expr) {
42
- return isRelation(expr) || isInOut(expr) || isIncoming(expr) || isOutgoing(expr) || isCustomRelationExpr(expr);
53
+ export function isRelationPredicateExpr(expr) {
54
+ return isRelationExpression(expr) || isRelationWhere(expr) || isCustomRelationExpr(expr);
43
55
  }
44
56
  // export const Expr = {
45
57
  // isElementRef,
@@ -3,6 +3,7 @@ export * from './element';
3
3
  export * from './expression';
4
4
  export * from './model';
5
5
  export * from './opaque';
6
+ export * from './operators';
6
7
  export * from './relation';
7
8
  export * from './theme';
8
9
  export * from './view';
@@ -3,6 +3,7 @@ export * from './element';
3
3
  export * from './expression';
4
4
  export * from './model';
5
5
  export * from './opaque';
6
+ export * from './operators';
6
7
  export * from './relation';
7
8
  export * from './theme';
8
9
  export * from './view';
@@ -0,0 +1,35 @@
1
+ import type { NonEmptyArray } from './_common';
2
+ export type EqualOperator<V> = {
3
+ eq: V;
4
+ } | {
5
+ neq: V;
6
+ };
7
+ export interface TagEqual {
8
+ tag: EqualOperator<string>;
9
+ }
10
+ export declare const isTagEqual: (operator: WhereOperator) => operator is TagEqual;
11
+ export interface KindEqual {
12
+ kind: EqualOperator<string>;
13
+ }
14
+ export declare const isKindEqual: (operator: WhereOperator) => operator is KindEqual;
15
+ export interface NotOperator<T> {
16
+ not: T;
17
+ }
18
+ export declare const isNotOperator: (operator: WhereOperator) => operator is NotOperator<WhereOperator>;
19
+ export interface AndOperator<T> {
20
+ and: NonEmptyArray<T>;
21
+ }
22
+ export declare const isAndOperator: (operator: WhereOperator) => operator is AndOperator<WhereOperator>;
23
+ export interface OrOperator<T> {
24
+ or: NonEmptyArray<T>;
25
+ }
26
+ export declare const isOrOperator: (operator: WhereOperator) => operator is OrOperator<WhereOperator>;
27
+ export type WhereOperator = TagEqual | KindEqual | NotOperator<WhereOperator> | AndOperator<WhereOperator> | OrOperator<WhereOperator>;
28
+ type Filterable<FTag extends string = string, FKind extends string = string> = {
29
+ tags?: FTag[] | null;
30
+ kind?: FKind;
31
+ };
32
+ type OperatorPredicate<V extends Filterable> = (value: V) => boolean;
33
+ export declare function whereOperatorAsPredicate<FTag extends string = string, FKind extends string = string>(operator: WhereOperator): OperatorPredicate<Filterable<FTag, FKind>>;
34
+ export {};
35
+ //# sourceMappingURL=operators.d.ts.map
@@ -0,0 +1,59 @@
1
+ import { allPass, anyPass, isNot, isNullish } from 'remeda';
2
+ import { nonexhaustive } from '../errors';
3
+ export const isTagEqual = (operator) => {
4
+ return 'tag' in operator;
5
+ };
6
+ export const isKindEqual = (operator) => {
7
+ return 'kind' in operator;
8
+ };
9
+ export const isNotOperator = (operator) => {
10
+ return 'not' in operator;
11
+ };
12
+ export const isAndOperator = (operator) => {
13
+ return 'and' in operator;
14
+ };
15
+ export const isOrOperator = (operator) => {
16
+ return 'or' in operator;
17
+ };
18
+ export function whereOperatorAsPredicate(operator) {
19
+ switch (true) {
20
+ case isTagEqual(operator): {
21
+ if ('eq' in operator.tag) {
22
+ const tag = operator.tag.eq;
23
+ return (value) => {
24
+ return Array.isArray(value.tags) && value.tags.includes(tag);
25
+ };
26
+ }
27
+ const tag = operator.tag.neq;
28
+ return (value) => {
29
+ return !Array.isArray(value.tags) || !value.tags.includes(tag);
30
+ };
31
+ }
32
+ case isKindEqual(operator): {
33
+ if ('eq' in operator.kind) {
34
+ const kind = operator.kind.eq;
35
+ return (value) => {
36
+ return value.kind === kind;
37
+ };
38
+ }
39
+ const kind = operator.kind.neq;
40
+ return (value) => {
41
+ return isNullish(value.kind) || value.kind !== kind;
42
+ };
43
+ }
44
+ case isNotOperator(operator): {
45
+ const predicate = whereOperatorAsPredicate(operator.not);
46
+ return isNot(predicate);
47
+ }
48
+ case isAndOperator(operator): {
49
+ const predicates = operator.and.map(whereOperatorAsPredicate);
50
+ return allPass(predicates);
51
+ }
52
+ case isOrOperator(operator): {
53
+ const predicates = operator.or.map(whereOperatorAsPredicate);
54
+ return anyPass(predicates);
55
+ }
56
+ default:
57
+ nonexhaustive(operator);
58
+ }
59
+ }
@@ -14,6 +14,8 @@ export interface Relation {
14
14
  readonly source: Fqn;
15
15
  readonly target: Fqn;
16
16
  readonly title: string;
17
+ readonly description?: string;
18
+ readonly technology?: string;
17
19
  readonly tags?: NonEmptyArray<Tag>;
18
20
  readonly kind?: RelationshipKind;
19
21
  readonly color?: ThemeColor;
@@ -1,7 +1,7 @@
1
- import type { NonEmptyArray, XYPosition } from './_common';
1
+ import type { NonEmptyArray } from './_common';
2
2
  import type { BorderStyle, ElementShape, Fqn } from './element';
3
3
  import type { ThemeColor } from './theme';
4
- import type { EdgeId } from './view';
4
+ import type { ViewManualLayout } from './view';
5
5
  export declare namespace ViewChanges {
6
6
  interface ChangeElementStyle {
7
7
  op: 'change-element-style';
@@ -15,15 +15,7 @@ export declare namespace ViewChanges {
15
15
  }
16
16
  interface SaveManualLayout {
17
17
  op: 'save-manual-layout';
18
- nodes: Record<Fqn, {
19
- x: number;
20
- y: number;
21
- width: number;
22
- height: number;
23
- }>;
24
- edges: Record<EdgeId, {
25
- controlPoints: XYPosition[];
26
- }>;
18
+ layout: ViewManualLayout;
27
19
  }
28
20
  }
29
21
  export type ViewChangeOp = ViewChanges.ChangeElementStyle | ViewChanges.SaveManualLayout;
@@ -1,4 +1,4 @@
1
- import type { IconUrl, NonEmptyArray, Point, XYPosition } from './_common';
1
+ import type { IconUrl, NonEmptyArray, Point, XYPoint } from './_common';
2
2
  import type { ElementKind, ElementShape, ElementStyle, Fqn, Tag } from './element';
3
3
  import type { ElementExpression, ElementPredicateExpression, Expression } from './expression';
4
4
  import type { Opaque } from './opaque';
@@ -43,9 +43,8 @@ export interface BasicView<ViewType extends 'element' | 'dynamic'> {
43
43
  /**
44
44
  * For all views we find common ancestor path.
45
45
  * This is used to generate relative paths, i.e.:
46
- * - "" for views in the common ancestor directory (or root)
47
- * - "subdir" for views in "<root>/subdir"
48
- * - "subdir/subdir1" for views in "<root>/subdir/subdir1"
46
+ * - "/home/project/index.c4" becomes "index.c4"
47
+ * - "/home/project/subdir/views.c4" becomes "subdir/views.c4"
49
48
  *
50
49
  * Undefined if the view is auto-generated.
51
50
  */
@@ -70,6 +69,12 @@ export interface DynamicViewStep {
70
69
  readonly source: Fqn;
71
70
  readonly target: Fqn;
72
71
  readonly title: string | null;
72
+ readonly description?: string;
73
+ readonly technology?: string;
74
+ readonly color?: ThemeColor;
75
+ readonly line?: RelationshipLineType;
76
+ readonly head?: RelationshipArrowType;
77
+ readonly tail?: RelationshipArrowType;
73
78
  readonly isBackward?: boolean;
74
79
  }
75
80
  export type DynamicViewIncludeRule = {
@@ -130,6 +135,8 @@ export interface ComputedEdge {
130
135
  source: NodeId;
131
136
  target: NodeId;
132
137
  label: string | null;
138
+ description?: string;
139
+ technology?: string;
133
140
  relations: RelationID[];
134
141
  color?: ThemeColor;
135
142
  line?: RelationshipLineType;
@@ -145,16 +152,31 @@ export interface ComputedEdge {
145
152
  */
146
153
  dir?: 'forward' | 'back';
147
154
  }
148
- export interface ComputedElementView extends Omit<ElementView, 'rules'> {
155
+ export interface ComputedElementView extends Omit<ElementView, 'rules' | 'docUri'> {
149
156
  readonly extends?: ViewID;
150
157
  readonly autoLayout: ViewRuleAutoLayout['autoLayout'];
151
158
  readonly nodes: ComputedNode[];
152
159
  readonly edges: ComputedEdge[];
160
+ rules?: never;
161
+ docUri?: never;
162
+ /**
163
+ * Hash of the view object.
164
+ * This is used to detect changes in layout
165
+ */
166
+ hash: string;
153
167
  }
154
- export interface ComputedDynamicView extends Omit<DynamicView, 'rules' | 'steps'> {
168
+ export interface ComputedDynamicView extends Omit<DynamicView, 'rules' | 'steps' | 'docUri'> {
155
169
  readonly autoLayout: ViewRuleAutoLayout['autoLayout'];
156
170
  readonly nodes: ComputedNode[];
157
171
  readonly edges: ComputedEdge[];
172
+ steps?: never;
173
+ rules?: never;
174
+ docUri?: never;
175
+ /**
176
+ * Hash of the view object.
177
+ * This is used to detect changes in layout
178
+ */
179
+ hash: string;
158
180
  }
159
181
  export declare function isComputedDynamicView(view: ComputedView): view is ComputedDynamicView;
160
182
  export type ComputedView = ComputedElementView | ComputedDynamicView;
@@ -165,6 +187,7 @@ export type BBox = {
165
187
  width: number;
166
188
  height: number;
167
189
  };
190
+ export declare function getBBoxCenter({ x, y, width, height }: BBox): XYPoint;
168
191
  export interface DiagramNode extends ComputedNode {
169
192
  width: number;
170
193
  height: number;
@@ -173,23 +196,40 @@ export interface DiagramNode extends ComputedNode {
173
196
  }
174
197
  export interface DiagramEdge extends ComputedEdge {
175
198
  points: NonEmptyArray<Point>;
199
+ controlPoints?: NonEmptyArray<XYPoint>;
176
200
  labelBBox?: BBox | null;
201
+ dotpos?: string;
177
202
  }
178
- export interface DiagramView extends Omit<ComputedView, 'nodes' | 'edges'> {
203
+ export interface DiagramView extends Omit<ComputedView, 'nodes' | 'edges' | 'manualLayout'> {
179
204
  readonly nodes: DiagramNode[];
180
205
  readonly edges: DiagramEdge[];
206
+ readonly bounds: BBox;
207
+ /**
208
+ * If diagram has manual layout
209
+ * But was changed and layout should be recalculated
210
+ */
211
+ hasLayoutDrift?: boolean;
212
+ manualLayout?: never;
213
+ }
214
+ export type ViewManualLayout = {
215
+ readonly hash: string;
216
+ readonly x: number;
217
+ readonly y: number;
181
218
  readonly width: number;
182
219
  readonly height: number;
183
- }
184
- export interface ViewManualLayout {
185
- readonly nodes: Record<Fqn, {
220
+ readonly autoLayout: AutoLayoutDirection;
221
+ readonly nodes: Record<string, {
222
+ isCompound: boolean;
186
223
  x: number;
187
224
  y: number;
188
225
  width: number;
189
226
  height: number;
190
227
  }>;
191
- readonly edges: Record<EdgeId, {
192
- controlPoints: XYPosition[];
228
+ readonly edges: Record<string, {
229
+ dotpos?: string;
230
+ points: NonEmptyArray<Point>;
231
+ controlPoints?: NonEmptyArray<XYPoint>;
232
+ labelBBox?: BBox;
193
233
  }>;
194
- }
234
+ };
195
235
  //# sourceMappingURL=view.d.ts.map
@@ -42,3 +42,9 @@ export function isComputedDynamicView(view) {
42
42
  export function isComputedElementView(view) {
43
43
  return isNullish(view.__) || view.__ === 'element';
44
44
  }
45
+ export function getBBoxCenter({ x, y, width, height }) {
46
+ return {
47
+ x: x + width / 2,
48
+ y: y + height / 2
49
+ };
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@likec4/core",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://likec4.dev",
6
6
  "author": "Denis Davydkov <denis@davydkov.com>",
@@ -71,11 +71,11 @@
71
71
  "type-fest": "^4.21.0"
72
72
  },
73
73
  "devDependencies": {
74
- "@likec4/tsconfig": "1.6.0",
74
+ "@likec4/tsconfig": "1.7.0",
75
75
  "@total-typescript/ts-reset": "^0.5.1",
76
76
  "execa": "^9.3.0",
77
- "typescript": "^5.5.3",
78
- "vitest": "~2.0.3"
77
+ "typescript": "^5.5.4",
78
+ "vitest": "~2.0.5"
79
79
  },
80
80
  "packageManager": "yarn@4.3.1"
81
81
  }
@@ -53,7 +53,7 @@ export const ElementColors = {
53
53
  hiContrast: '#FBD3CB',
54
54
  // hiContrast: '#f8fafc',
55
55
  // loContrast: '#fdd8d8' // radix black red 12
56
- loContrast: '#FF977D'
56
+ loContrast: '#f5b2a3'
57
57
  },
58
58
  green: {
59
59
  fill: '#428a4f',
@@ -46,7 +46,7 @@ export const RelationshipColors = {
46
46
  red: {
47
47
  lineColor: '#AC4D39',
48
48
  labelBgColor: '#b91c1c',
49
- labelColor: '#FF977D'
49
+ labelColor: '#f5b2a3'
50
50
  },
51
51
  secondary: sky,
52
52
  sky,
@@ -6,7 +6,7 @@ export type IconUrl = Opaque<string, 'IconUrl'>
6
6
 
7
7
  export type Point = readonly [x: number, y: number]
8
8
 
9
- export interface XYPosition {
9
+ export interface XYPoint {
10
10
  x: number
11
11
  y: number
12
12
  }
@@ -1,10 +1,12 @@
1
1
  import type { IconUrl } from './_common'
2
2
  import type { BorderStyle, ElementKind, ElementShape, Fqn, Tag } from './element'
3
+ import type { WhereOperator } from './operators'
3
4
  import type { RelationshipArrowType, RelationshipLineType } from './relation'
4
5
  import type { ThemeColor } from './theme'
5
6
  import type { ViewID } from './view'
6
7
 
7
8
  interface BaseExpr {
9
+ where?: never
8
10
  element?: never
9
11
  custom?: never
10
12
  expanded?: never
@@ -38,7 +40,7 @@ export function isExpandedElementExpr(expr: Expression): expr is ExpandedElement
38
40
 
39
41
  export interface CustomElementExpr extends Omit<BaseExpr, 'custom'> {
40
42
  custom: {
41
- element: Fqn
43
+ expr: ElementExpression | ElementWhereExpr
42
44
  title?: string
43
45
  description?: string
44
46
  technology?: string
@@ -52,7 +54,7 @@ export interface CustomElementExpr extends Omit<BaseExpr, 'custom'> {
52
54
  }
53
55
 
54
56
  export function isCustomElement(expr: Expression): expr is CustomElementExpr {
55
- return 'custom' in expr
57
+ return 'custom' in expr && (isElement(expr.custom.expr) || isElementWhere(expr.custom.expr))
56
58
  }
57
59
 
58
60
  export interface WildcardExpr extends Omit<BaseExpr, 'wildcard'> {
@@ -91,7 +93,20 @@ export function isElement(expr: Expression): expr is ElementExpression {
91
93
  || isExpandedElementExpr(expr)
92
94
  }
93
95
 
94
- export type ElementPredicateExpression = ElementExpression | CustomElementExpr
96
+ export interface ElementWhereExpr extends Omit<BaseExpr, 'where'> {
97
+ where: {
98
+ expr: ElementExpression
99
+ condition: WhereOperator
100
+ }
101
+ }
102
+ export function isElementWhere(expr: Expression): expr is ElementWhereExpr {
103
+ return 'where' in expr && isElement(expr.where.expr)
104
+ }
105
+
106
+ export type ElementPredicateExpression = ElementExpression | ElementWhereExpr | CustomElementExpr
107
+ export function isElementPredicateExpr(expr: Expression): expr is ElementPredicateExpression {
108
+ return isElement(expr) || isElementWhere(expr) || isCustomElement(expr)
109
+ }
95
110
 
96
111
  export interface RelationExpr extends Omit<BaseExpr, 'source' | 'target'> {
97
112
  source: ElementExpression
@@ -122,10 +137,28 @@ export function isOutgoing(expr: Expression): expr is OutgoingExpr {
122
137
  return 'outgoing' in expr
123
138
  }
124
139
 
140
+ export type RelationExpression = RelationExpr | InOutExpr | IncomingExpr | OutgoingExpr
141
+
142
+ export function isRelationExpression(expr: Expression): expr is RelationExpression {
143
+ return isRelation(expr) || isInOut(expr) || isIncoming(expr) || isOutgoing(expr)
144
+ }
145
+
146
+ export interface RelationWhereExpr extends Omit<BaseExpr, 'where'> {
147
+ where: {
148
+ expr: RelationExpression
149
+ condition: WhereOperator
150
+ }
151
+ }
152
+ export function isRelationWhere(expr: Expression): expr is RelationWhereExpr {
153
+ return 'where' in expr && isRelationExpression(expr.where.expr)
154
+ }
155
+
125
156
  export interface CustomRelationExpr extends Omit<BaseExpr, 'customRelation'> {
126
157
  customRelation: {
127
- relation: RelationExpr
158
+ relation: RelationExpression | RelationWhereExpr
128
159
  title?: string
160
+ description?: string
161
+ technology?: string
129
162
  color?: ThemeColor
130
163
  line?: RelationshipLineType
131
164
  head?: RelationshipArrowType
@@ -136,12 +169,10 @@ export function isCustomRelationExpr(expr: Expression): expr is CustomRelationEx
136
169
  return 'customRelation' in expr
137
170
  }
138
171
 
139
- export type RelationExpression = RelationExpr | InOutExpr | IncomingExpr | OutgoingExpr
140
-
141
- export type RelationPredicateExpression = RelationExpression | CustomRelationExpr
172
+ export type RelationPredicateExpression = RelationExpression | RelationWhereExpr | CustomRelationExpr
142
173
 
143
- export function isAnyRelation(expr: Expression): expr is RelationPredicateExpression {
144
- return isRelation(expr) || isInOut(expr) || isIncoming(expr) || isOutgoing(expr) || isCustomRelationExpr(expr)
174
+ export function isRelationPredicateExpr(expr: Expression): expr is RelationPredicateExpression {
175
+ return isRelationExpression(expr) || isRelationWhere(expr) || isCustomRelationExpr(expr)
145
176
  }
146
177
 
147
178
  export type Expression = ElementPredicateExpression | RelationPredicateExpression
@@ -3,6 +3,7 @@ export * from './element'
3
3
  export * from './expression'
4
4
  export * from './model'
5
5
  export * from './opaque'
6
+ export * from './operators'
6
7
  export * from './relation'
7
8
  export * from './theme'
8
9
  export * from './view'
@@ -0,0 +1,114 @@
1
+ import { allPass, anyPass, isNot, isNullish } from 'remeda'
2
+ import { nonexhaustive } from '../errors'
3
+ import type { NonEmptyArray } from './_common'
4
+
5
+ export type EqualOperator<V> = {
6
+ eq: V
7
+ } | {
8
+ neq: V
9
+ }
10
+
11
+ export interface TagEqual {
12
+ tag: EqualOperator<string>
13
+ }
14
+ export const isTagEqual = (operator: WhereOperator): operator is TagEqual => {
15
+ return 'tag' in operator
16
+ }
17
+
18
+ export interface KindEqual {
19
+ kind: EqualOperator<string>
20
+ }
21
+ export const isKindEqual = (operator: WhereOperator): operator is KindEqual => {
22
+ return 'kind' in operator
23
+ }
24
+
25
+ export interface NotOperator<T> {
26
+ not: T
27
+ }
28
+ export const isNotOperator = (operator: WhereOperator): operator is NotOperator<WhereOperator> => {
29
+ return 'not' in operator
30
+ }
31
+
32
+ export interface AndOperator<T> {
33
+ and: NonEmptyArray<T>
34
+ }
35
+ export const isAndOperator = (operator: WhereOperator): operator is AndOperator<WhereOperator> => {
36
+ return 'and' in operator
37
+ }
38
+
39
+ export interface OrOperator<T> {
40
+ or: NonEmptyArray<T>
41
+ }
42
+ export const isOrOperator = (operator: WhereOperator): operator is OrOperator<WhereOperator> => {
43
+ return 'or' in operator
44
+ }
45
+
46
+ export type WhereOperator =
47
+ | TagEqual
48
+ | KindEqual
49
+ | NotOperator<WhereOperator>
50
+ | AndOperator<WhereOperator>
51
+ | OrOperator<WhereOperator>
52
+ // type Ops<T> = UnionToIntersection<Op<T>>
53
+ // type Op1<T> = RequireExactlyOne<T>
54
+
55
+ // export type Operator1<T> = T extends Op<T> ? Op1<Ops<T>> : never
56
+ // export type Operator = Operator1<any>
57
+
58
+ // type L = keyof UnionToIntersection<Op<any>>
59
+
60
+ type Filterable<
61
+ FTag extends string = string,
62
+ FKind extends string = string
63
+ > = {
64
+ tags?: FTag[] | null
65
+ kind?: FKind
66
+ }
67
+
68
+ type OperatorPredicate<V extends Filterable> = (value: V) => boolean
69
+
70
+ export function whereOperatorAsPredicate<
71
+ FTag extends string = string,
72
+ FKind extends string = string
73
+ >(operator: WhereOperator): OperatorPredicate<Filterable<FTag, FKind>> {
74
+ switch (true) {
75
+ case isTagEqual(operator): {
76
+ if ('eq' in operator.tag) {
77
+ const tag = operator.tag.eq
78
+ return (value) => {
79
+ return Array.isArray(value.tags) && value.tags.includes(tag as FTag)
80
+ }
81
+ }
82
+ const tag = operator.tag.neq
83
+ return (value) => {
84
+ return !Array.isArray(value.tags) || !value.tags.includes(tag as FTag)
85
+ }
86
+ }
87
+ case isKindEqual(operator): {
88
+ if ('eq' in operator.kind) {
89
+ const kind = operator.kind.eq
90
+ return (value) => {
91
+ return value.kind === kind
92
+ }
93
+ }
94
+ const kind = operator.kind.neq
95
+ return (value) => {
96
+ return isNullish(value.kind) || value.kind !== kind
97
+ }
98
+ }
99
+ case isNotOperator(operator): {
100
+ const predicate = whereOperatorAsPredicate(operator.not)
101
+ return isNot(predicate)
102
+ }
103
+ case isAndOperator(operator): {
104
+ const predicates = operator.and.map(whereOperatorAsPredicate)
105
+ return allPass(predicates)
106
+ }
107
+ case isOrOperator(operator): {
108
+ const predicates = operator.or.map(whereOperatorAsPredicate)
109
+ return anyPass(predicates)
110
+ }
111
+ default:
112
+ nonexhaustive(operator)
113
+ }
114
+ }
@@ -30,6 +30,8 @@ export interface Relation {
30
30
  readonly source: Fqn
31
31
  readonly target: Fqn
32
32
  readonly title: string
33
+ readonly description?: string
34
+ readonly technology?: string
33
35
  readonly tags?: NonEmptyArray<Tag>
34
36
  readonly kind?: RelationshipKind
35
37
  readonly color?: ThemeColor
@@ -1,7 +1,7 @@
1
- import type { NonEmptyArray, XYPosition } from './_common'
1
+ import type { NonEmptyArray, XYPoint } from './_common'
2
2
  import type { BorderStyle, ElementShape, Fqn } from './element'
3
3
  import type { ThemeColor } from './theme'
4
- import type { EdgeId } from './view'
4
+ import type { EdgeId, ViewManualLayout } from './view'
5
5
 
6
6
  export namespace ViewChanges {
7
7
  export interface ChangeElementStyle {
@@ -17,15 +17,7 @@ export namespace ViewChanges {
17
17
 
18
18
  export interface SaveManualLayout {
19
19
  op: 'save-manual-layout'
20
- nodes: Record<Fqn, {
21
- x: number
22
- y: number
23
- width: number
24
- height: number
25
- }>
26
- edges: Record<EdgeId, {
27
- controlPoints: XYPosition[]
28
- }>
20
+ layout: ViewManualLayout
29
21
  }
30
22
  }
31
23
  export type ViewChangeOp = ViewChanges.ChangeElementStyle | ViewChanges.SaveManualLayout
package/src/types/view.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isNullish } from 'remeda'
2
- import type { IconUrl, NonEmptyArray, Point, XYPosition } from './_common'
2
+ import type { IconUrl, NonEmptyArray, Point, XYPoint } from './_common'
3
3
  import type { ElementKind, ElementShape, ElementStyle, Fqn, Tag } from './element'
4
4
  import type { ElementExpression, ElementPredicateExpression, Expression } from './expression'
5
5
  import type { Opaque } from './opaque'
@@ -63,9 +63,8 @@ export interface BasicView<ViewType extends 'element' | 'dynamic'> {
63
63
  /**
64
64
  * For all views we find common ancestor path.
65
65
  * This is used to generate relative paths, i.e.:
66
- * - "" for views in the common ancestor directory (or root)
67
- * - "subdir" for views in "<root>/subdir"
68
- * - "subdir/subdir1" for views in "<root>/subdir/subdir1"
66
+ * - "/home/project/index.c4" becomes "index.c4"
67
+ * - "/home/project/subdir/views.c4" becomes "subdir/views.c4"
69
68
  *
70
69
  * Undefined if the view is auto-generated.
71
70
  */
@@ -94,6 +93,12 @@ export interface DynamicViewStep {
94
93
  readonly source: Fqn
95
94
  readonly target: Fqn
96
95
  readonly title: string | null
96
+ readonly description?: string
97
+ readonly technology?: string
98
+ readonly color?: ThemeColor
99
+ readonly line?: RelationshipLineType
100
+ readonly head?: RelationshipArrowType
101
+ readonly tail?: RelationshipArrowType
97
102
  readonly isBackward?: boolean
98
103
  }
99
104
 
@@ -188,6 +193,8 @@ export interface ComputedEdge {
188
193
  source: NodeId
189
194
  target: NodeId
190
195
  label: string | null
196
+ description?: string
197
+ technology?: string
191
198
  relations: RelationID[]
192
199
  color?: ThemeColor
193
200
  line?: RelationshipLineType
@@ -204,16 +211,33 @@ export interface ComputedEdge {
204
211
  dir?: 'forward' | 'back'
205
212
  }
206
213
 
207
- export interface ComputedElementView extends Omit<ElementView, 'rules'> {
214
+ export interface ComputedElementView extends Omit<ElementView, 'rules' | 'docUri'> {
208
215
  readonly extends?: ViewID
209
216
  readonly autoLayout: ViewRuleAutoLayout['autoLayout']
210
217
  readonly nodes: ComputedNode[]
211
218
  readonly edges: ComputedEdge[]
219
+ rules?: never
220
+ docUri?: never
221
+
222
+ /**
223
+ * Hash of the view object.
224
+ * This is used to detect changes in layout
225
+ */
226
+ hash: string
212
227
  }
213
- export interface ComputedDynamicView extends Omit<DynamicView, 'rules' | 'steps'> {
228
+ export interface ComputedDynamicView extends Omit<DynamicView, 'rules' | 'steps' | 'docUri'> {
214
229
  readonly autoLayout: ViewRuleAutoLayout['autoLayout']
215
230
  readonly nodes: ComputedNode[]
216
231
  readonly edges: ComputedEdge[]
232
+ steps?: never
233
+ rules?: never
234
+ docUri?: never
235
+
236
+ /**
237
+ * Hash of the view object.
238
+ * This is used to detect changes in layout
239
+ */
240
+ hash: string
217
241
  }
218
242
  export function isComputedDynamicView(view: ComputedView): view is ComputedDynamicView {
219
243
  return view.__ === 'dynamic'
@@ -233,6 +257,18 @@ export type BBox = {
233
257
  height: number
234
258
  }
235
259
 
260
+ export function getBBoxCenter({
261
+ x,
262
+ y,
263
+ width,
264
+ height
265
+ }: BBox): XYPoint {
266
+ return {
267
+ x: x + width / 2,
268
+ y: y + height / 2
269
+ }
270
+ }
271
+
236
272
  export interface DiagramNode extends ComputedNode {
237
273
  width: number
238
274
  height: number
@@ -242,25 +278,52 @@ export interface DiagramNode extends ComputedNode {
242
278
  }
243
279
 
244
280
  export interface DiagramEdge extends ComputedEdge {
281
+ // Bezier points
245
282
  points: NonEmptyArray<Point>
283
+ // Control points to adjust the edge
284
+ controlPoints?: NonEmptyArray<XYPoint>
246
285
  labelBBox?: BBox | null
286
+ // Graphviz edge POS
287
+ // TODO: temporary solution, should be moved out
288
+ dotpos?: string
247
289
  }
248
290
 
249
- export interface DiagramView extends Omit<ComputedView, 'nodes' | 'edges'> {
291
+ export interface DiagramView extends Omit<ComputedView, 'nodes' | 'edges' | 'manualLayout'> {
250
292
  readonly nodes: DiagramNode[]
251
293
  readonly edges: DiagramEdge[]
252
- readonly width: number
253
- readonly height: number
294
+ readonly bounds: BBox
295
+
296
+ /**
297
+ * If diagram has manual layout
298
+ * But was changed and layout should be recalculated
299
+ */
300
+ hasLayoutDrift?: boolean
301
+ // Should not exist in DiagramView
302
+ manualLayout?: never
254
303
  }
255
304
 
256
- export interface ViewManualLayout {
257
- readonly nodes: Record<Fqn, {
305
+ export type ViewManualLayout = {
306
+ // Object hash of previous layout
307
+ readonly hash: string
308
+ readonly x: number
309
+ readonly y: number
310
+ readonly width: number
311
+ readonly height: number
312
+ readonly autoLayout: AutoLayoutDirection
313
+ readonly nodes: Record<string, {
314
+ isCompound: boolean
258
315
  x: number
259
316
  y: number
260
317
  width: number
261
318
  height: number
262
319
  }>
263
- readonly edges: Record<EdgeId, {
264
- controlPoints: XYPosition[]
320
+ readonly edges: Record<string, {
321
+ // Graphviz edge POS
322
+ dotpos?: string
323
+ // Bezier points
324
+ points: NonEmptyArray<Point>
325
+ // Control points to adjust the edge
326
+ controlPoints?: NonEmptyArray<XYPoint>
327
+ labelBBox?: BBox
265
328
  }>
266
329
  }