dsh-model-everything 0.5.0 → 0.5.2
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.
- package/README.en-US.md +12 -0
- package/README.md +4 -0
- package/lib/client.js +2829 -224
- package/lib/render/channels.js +243 -0
- package/lib/render/collapse.js +12 -20
- package/lib/render/components.js +99 -0
- package/lib/render/coords.js +288 -0
- package/lib/render/cycle-break.js +147 -0
- package/lib/render/dummy.js +43 -0
- package/lib/render/from-mermaid.js +34 -10
- package/lib/render/from-plantuml.js +318 -46
- package/lib/render/layer.js +91 -0
- package/lib/render/layout.js +27 -11
- package/lib/render/network-simplex.js +272 -0
- package/lib/render/order.js +122 -0
- package/lib/render/paint.js +1227 -69
- package/lib/render/place.js +393 -121
- package/lib/render/route.js +225 -0
- package/lib/render/svg.js +146 -21
- package/lib/render/symbol.js +108 -0
- package/lib/render/text.js +74 -0
- package/lib/render/trunk.js +213 -0
- package/lib/render/wrap.js +87 -0
- package/lib/types/render/channels.d.ts +63 -0
- package/lib/types/render/components.d.ts +57 -0
- package/lib/types/render/coords.d.ts +64 -0
- package/lib/types/render/cycle-break.d.ts +45 -0
- package/lib/types/render/dummy.d.ts +32 -0
- package/lib/types/render/from-plantuml.d.ts +12 -6
- package/lib/types/render/layer.d.ts +28 -0
- package/lib/types/render/layout.d.ts +43 -11
- package/lib/types/render/model.d.ts +17 -0
- package/lib/types/render/network-simplex.d.ts +54 -0
- package/lib/types/render/order.d.ts +27 -0
- package/lib/types/render/paint.d.ts +157 -10
- package/lib/types/render/place.d.ts +40 -19
- package/lib/types/render/route.d.ts +77 -0
- package/lib/types/render/svg.d.ts +1 -1
- package/lib/types/render/symbol.d.ts +73 -0
- package/lib/types/render/text.d.ts +38 -0
- package/lib/types/render/trunk.d.ts +104 -0
- package/lib/types/render/wrap.d.ts +27 -0
- package/package.json +148 -148
- package/skills/model-everything-diagram/SKILL.md +85 -82
- package/src/render/channels.ts +309 -0
- package/src/render/collapse.ts +150 -155
- package/src/render/components.ts +126 -0
- package/src/render/coords.ts +328 -0
- package/src/render/cycle-break.ts +164 -0
- package/src/render/dummy.ts +65 -0
- package/src/render/from-mermaid.ts +261 -236
- package/src/render/from-plantuml.ts +469 -178
- package/src/render/layer.ts +98 -0
- package/src/render/layout.ts +101 -62
- package/src/render/model.ts +86 -67
- package/src/render/network-simplex.ts +290 -0
- package/src/render/order.ts +139 -0
- package/src/render/paint.ts +1570 -290
- package/src/render/place.ts +532 -234
- package/src/render/route.ts +269 -0
- package/src/render/svg.ts +274 -127
- package/src/render/symbol.ts +137 -0
- package/src/render/text.ts +86 -0
- package/src/render/trunk.ts +275 -0
- package/src/render/wrap.ts +103 -0
package/src/render/collapse.ts
CHANGED
|
@@ -1,155 +1,150 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Collapsing a group: the drawing with one boundary folded into a single box.
|
|
3
|
-
*
|
|
4
|
-
* A fold is a transform on the model, not a mode of the painter. That keeps the painter's rules unchanged -
|
|
5
|
-
* it still draws boxes and edges it is given - and it makes the behaviour testable without a browser: feed a
|
|
6
|
-
* diagram in, assert what came out.
|
|
7
|
-
*
|
|
8
|
-
* Folding is what makes a hierarchy usable: a reader who has understood a package wants it out of the way, and
|
|
9
|
-
* an architecture diagram that cannot be folded is read at one zoom level forever.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { Diagram, DiagramEdge, DiagramGroup, DiagramNode } from './model.js'
|
|
13
|
-
|
|
14
|
-
/** How many nodes a group holds, counting the whole subtree - what the folded box offers to unfold. */
|
|
15
|
-
function subtreeSize(
|
|
16
|
-
if (seen.has(groupId)) return 0
|
|
17
|
-
seen.add(groupId)
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
edges,
|
|
152
|
-
...(remaining.length === 0 ? { groups: undefined } : { groups: remaining }),
|
|
153
|
-
caption,
|
|
154
|
-
}
|
|
155
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Collapsing a group: the drawing with one boundary folded into a single box.
|
|
3
|
+
*
|
|
4
|
+
* A fold is a transform on the model, not a mode of the painter. That keeps the painter's rules unchanged -
|
|
5
|
+
* it still draws boxes and edges it is given - and it makes the behaviour testable without a browser: feed a
|
|
6
|
+
* diagram in, assert what came out.
|
|
7
|
+
*
|
|
8
|
+
* Folding is what makes a hierarchy usable: a reader who has understood a package wants it out of the way, and
|
|
9
|
+
* an architecture diagram that cannot be folded is read at one zoom level forever.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Diagram, DiagramEdge, DiagramGroup, DiagramNode } from './model.js'
|
|
13
|
+
|
|
14
|
+
/** How many nodes a group holds, counting the whole subtree - what the folded box offers to unfold. */
|
|
15
|
+
function subtreeSize(groups: DiagramGroup[], groupId: string, seen: Set<string>): number {
|
|
16
|
+
if (seen.has(groupId)) return 0
|
|
17
|
+
seen.add(groupId)
|
|
18
|
+
const group = groups.find((entry) => entry.id === groupId) as DiagramGroup
|
|
19
|
+
const children = groups.filter((entry) => entry.parent === groupId)
|
|
20
|
+
return group.members.length + children.reduce((total, child) => total + subtreeSize(groups, child.id, seen), 0)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The group that directly holds a node, when one does. */
|
|
24
|
+
function ownerOf(groups: DiagramGroup[], nodeId: string): string | undefined {
|
|
25
|
+
return groups.find((group) => group.members.includes(nodeId))?.id
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fold the named groups.
|
|
30
|
+
*
|
|
31
|
+
* A folded group becomes a node carrying its name and how many nodes it holds; its members - and every group
|
|
32
|
+
* inside it - leave the drawing. An edge that pointed at something inside the fold is re-aimed at the fold's
|
|
33
|
+
* box, and an edge between two members of the same fold disappears: both ends are now the same box, and a
|
|
34
|
+
* self-loop would be a claim the source never made.
|
|
35
|
+
*
|
|
36
|
+
* Ids nobody declared are ignored, and folding a group whose contents are already gone changes nothing, so the
|
|
37
|
+
* function is total on any diagram and any set of names.
|
|
38
|
+
*/
|
|
39
|
+
export function collapse(diagram: Diagram, folded: Iterable<string>): Diagram {
|
|
40
|
+
const asked = new Set(folded)
|
|
41
|
+
const groups: DiagramGroup[] = diagram.groups ?? []
|
|
42
|
+
if (asked.size === 0 || groups.length === 0) return diagram
|
|
43
|
+
|
|
44
|
+
// Only groups that exist, and that still have contents, can be folded.
|
|
45
|
+
const byId = new Map(groups.map((group) => [group.id, group] as const))
|
|
46
|
+
const targets = new Set<string>()
|
|
47
|
+
for (const id of asked) {
|
|
48
|
+
const group = byId.get(id)
|
|
49
|
+
if (group !== undefined && subtreeSize(groups, id, new Set()) > 0) targets.add(id)
|
|
50
|
+
}
|
|
51
|
+
if (targets.size === 0) return diagram
|
|
52
|
+
|
|
53
|
+
// A fold inside another fold is served by the outer one: the inner box would not be drawn anyway.
|
|
54
|
+
for (const id of [...targets]) {
|
|
55
|
+
let cursor = byId.get(id)?.parent
|
|
56
|
+
const seen = new Set<string>()
|
|
57
|
+
while (cursor !== undefined && !seen.has(cursor)) {
|
|
58
|
+
seen.add(cursor)
|
|
59
|
+
if (targets.has(cursor)) {
|
|
60
|
+
targets.delete(id)
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
cursor = byId.get(cursor)?.parent
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const removedNodes = new Set<string>()
|
|
68
|
+
const removedGroups = new Set<string>()
|
|
69
|
+
const descend = (groupId: string): void => {
|
|
70
|
+
removedGroups.add(groupId)
|
|
71
|
+
const group = byId.get(groupId) as DiagramGroup
|
|
72
|
+
for (const member of group.members) removedNodes.add(member)
|
|
73
|
+
for (const child of groups.filter((entry) => entry.parent === groupId)) descend(child.id)
|
|
74
|
+
}
|
|
75
|
+
for (const id of targets) descend(id)
|
|
76
|
+
|
|
77
|
+
/** The innermost fold that swallowed this id, if any. */
|
|
78
|
+
const foldOf = (id: string): string | undefined => {
|
|
79
|
+
for (const target of targets) {
|
|
80
|
+
const inSubtree = (groupId: string, seen: Set<string>): boolean => {
|
|
81
|
+
if (seen.has(groupId)) return false
|
|
82
|
+
seen.add(groupId)
|
|
83
|
+
if (groupId === target) return true
|
|
84
|
+
return groups.filter((entry) => entry.parent === groupId).some((child) => inSubtree(child.id, seen))
|
|
85
|
+
}
|
|
86
|
+
const group = byId.get(target) as DiagramGroup
|
|
87
|
+
if (group.members.includes(id) || inSubtree(ownerOf(groups, id) as string, new Set())) return target
|
|
88
|
+
}
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The box that replaces a fold takes the place of its first member, so the drawing keeps the order the source
|
|
93
|
+
// gave it: a folded package belongs where the package was, not at the end of the page.
|
|
94
|
+
const nodes: DiagramNode[] = []
|
|
95
|
+
const emitted = new Set<string>()
|
|
96
|
+
for (const node of diagram.nodes) {
|
|
97
|
+
if (!removedNodes.has(node.id)) {
|
|
98
|
+
nodes.push(node)
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
const fold = foldOf(node.id)
|
|
102
|
+
if (fold === undefined || emitted.has(fold)) continue
|
|
103
|
+
emitted.add(fold)
|
|
104
|
+
const group = byId.get(fold) as DiagramGroup
|
|
105
|
+
nodes.push({ id: fold, label: group.label + ' (' + String(subtreeSize(groups, fold, new Set())) + ')', role: 'normal' })
|
|
106
|
+
}
|
|
107
|
+
// A fold whose members are not in `nodes` at all still deserves a box rather than vanishing.
|
|
108
|
+
for (const id of targets) {
|
|
109
|
+
if (emitted.has(id)) continue
|
|
110
|
+
const group = byId.get(id) as DiagramGroup
|
|
111
|
+
nodes.push({ id, label: group.label + ' (' + String(subtreeSize(groups, id, new Set())) + ')', role: 'normal' })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const edges: DiagramEdge[] = []
|
|
115
|
+
const seenEdges = new Set<string>()
|
|
116
|
+
for (const edge of diagram.edges) {
|
|
117
|
+
const from = removedNodes.has(edge.from) ? foldOf(edge.from) : edge.from
|
|
118
|
+
const to = removedNodes.has(edge.to) ? foldOf(edge.to) : edge.to
|
|
119
|
+
if (from === undefined || to === undefined) continue
|
|
120
|
+
if (from === to) continue
|
|
121
|
+
const key = from + '>' + to + ':' + edge.label
|
|
122
|
+
if (seenEdges.has(key)) continue
|
|
123
|
+
seenEdges.add(key)
|
|
124
|
+
edges.push({ ...edge, from, to })
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A folded box stands where its group stood, so it belongs to that group's parent: without this the box that
|
|
128
|
+
// replaced an inner boundary would be drawn outside the outer one, which reads as "it left the package".
|
|
129
|
+
const adopted = new Map<string, string[]>()
|
|
130
|
+
for (const id of targets) {
|
|
131
|
+
const parent = byId.get(id)?.parent
|
|
132
|
+
if (parent === undefined) continue
|
|
133
|
+
adopted.set(parent, [...(adopted.get(parent) ?? []), id])
|
|
134
|
+
}
|
|
135
|
+
const remaining = groups
|
|
136
|
+
.filter((group) => !removedGroups.has(group.id) && targets.has(group.id) === false)
|
|
137
|
+
.map((group) => {
|
|
138
|
+
const extra = adopted.get(group.id)
|
|
139
|
+
if (extra === undefined) return group
|
|
140
|
+
return { ...group, members: [...group.members.filter((member) => !removedNodes.has(member)), ...extra] }
|
|
141
|
+
})
|
|
142
|
+
const caption = diagram.caption + (targets.size === 1 ? ', 1 group folded' : ', ' + String(targets.size) + ' groups folded')
|
|
143
|
+
return {
|
|
144
|
+
...diagram,
|
|
145
|
+
nodes,
|
|
146
|
+
edges,
|
|
147
|
+
...(remaining.length === 0 ? { groups: undefined } : { groups: remaining }),
|
|
148
|
+
caption,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two decompositions a drawing needs before it can be packed.
|
|
3
|
+
*
|
|
4
|
+
* **Weakly connected components.** Two subgraphs with no edge between them are two drawings that happen to share a
|
|
5
|
+
* page. Laying them out together makes them share one set of layers, so the wider one sets every column width and
|
|
6
|
+
* the narrower one is squeezed beside it; moving one moves the other. Split first, lay each out on its own, then pack
|
|
7
|
+
* the rectangles.
|
|
8
|
+
*
|
|
9
|
+
* **Packing.** A pile of rectangles has to become a page. The target is the aspect ratio a page and a screen share -
|
|
10
|
+
* 1.6 - and a shelf packer is enough to get there: sort by height, fill a shelf until the next rectangle would make
|
|
11
|
+
* the drawing wider than the page it is aiming at, then start the next shelf. No overlap, and a deterministic order,
|
|
12
|
+
* so the same rectangles always pack the same way.
|
|
13
|
+
*
|
|
14
|
+
* Pure, and asserted on the rectangles themselves.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface SizedRectangle {
|
|
18
|
+
width: number
|
|
19
|
+
height: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PackedRectangle {
|
|
23
|
+
x: number
|
|
24
|
+
y: number
|
|
25
|
+
width: number
|
|
26
|
+
height: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The aspect ratio a page and a landscape screen share. */
|
|
30
|
+
const PAGE_ASPECT = 1.6
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The nodes of each weakly connected component, in the order the nodes were written.
|
|
34
|
+
*
|
|
35
|
+
* An isolated node is a component of its own: it is not noise, it is a drawing with nothing joining it, and the
|
|
36
|
+
* packer is what decides where it goes.
|
|
37
|
+
*/
|
|
38
|
+
export function weakComponents(
|
|
39
|
+
nodes: ReadonlyArray<{ id: string }>,
|
|
40
|
+
edges: ReadonlyArray<{ from: string; to: string }>,
|
|
41
|
+
): string[][] {
|
|
42
|
+
const known = new Set(nodes.map((node) => node.id))
|
|
43
|
+
const neighbours = new Map<string, string[]>()
|
|
44
|
+
const link = (from: string, to: string): void => {
|
|
45
|
+
neighbours.set(from, [...(neighbours.get(from) ?? []), to])
|
|
46
|
+
}
|
|
47
|
+
for (const edge of edges) {
|
|
48
|
+
if (!known.has(edge.from) || !known.has(edge.to) || edge.from === edge.to) continue
|
|
49
|
+
link(edge.from, edge.to)
|
|
50
|
+
link(edge.to, edge.from)
|
|
51
|
+
}
|
|
52
|
+
const seen = new Set<string>()
|
|
53
|
+
const components: string[][] = []
|
|
54
|
+
for (const node of nodes) {
|
|
55
|
+
if (seen.has(node.id)) continue
|
|
56
|
+
const members: string[] = []
|
|
57
|
+
const queue = [node.id]
|
|
58
|
+
seen.add(node.id)
|
|
59
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
60
|
+
const id = queue[head]
|
|
61
|
+
members.push(id)
|
|
62
|
+
for (const next of neighbours.get(id) ?? []) {
|
|
63
|
+
if (seen.has(next)) continue
|
|
64
|
+
seen.add(next)
|
|
65
|
+
queue.push(next)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
components.push(members)
|
|
69
|
+
}
|
|
70
|
+
return components
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface PackOptions {
|
|
74
|
+
/** The empty strip kept between two rectangles, on both axes. */
|
|
75
|
+
gap?: number
|
|
76
|
+
/** The page the packer aims at; the drawing comes out as close to it as the rectangles allow. */
|
|
77
|
+
aspect?: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Pack rectangles into a page of roughly `aspect`, using shelves.
|
|
82
|
+
*
|
|
83
|
+
* The shelf width is derived from the total area and the target aspect: that is the width at which a perfect grid
|
|
84
|
+
* would have the wanted shape, so a shelf that fills it lands near the target. Rectangles are taken tallest first,
|
|
85
|
+
* which is what keeps a shelf from being mostly empty air above a short rectangle.
|
|
86
|
+
*/
|
|
87
|
+
export function packRectangles(rectangles: ReadonlyArray<SizedRectangle>, options: PackOptions = {}): {
|
|
88
|
+
placements: PackedRectangle[]
|
|
89
|
+
width: number
|
|
90
|
+
height: number
|
|
91
|
+
} {
|
|
92
|
+
const gap = options.gap ?? 30
|
|
93
|
+
const aspect = options.aspect ?? PAGE_ASPECT
|
|
94
|
+
if (rectangles.length === 0) return { placements: [], width: 0, height: 0 }
|
|
95
|
+
|
|
96
|
+
const area = rectangles.reduce((total, rectangle) => total + (rectangle.width + gap) * (rectangle.height + gap), 0)
|
|
97
|
+
const targetWidth = Math.max(gap, Math.sqrt(area * aspect))
|
|
98
|
+
const order = rectangles
|
|
99
|
+
.map((rectangle, index) => ({ rectangle, index }))
|
|
100
|
+
.sort((left, right) => right.rectangle.height - left.rectangle.height || right.rectangle.width - left.rectangle.width || left.index - right.index)
|
|
101
|
+
|
|
102
|
+
const placements: PackedRectangle[] = rectangles.map((rectangle) => ({ x: 0, y: 0, width: rectangle.width, height: rectangle.height }))
|
|
103
|
+
let x = 0
|
|
104
|
+
let y = 0
|
|
105
|
+
let shelfHeight = 0
|
|
106
|
+
let width = 0
|
|
107
|
+
for (const entry of order) {
|
|
108
|
+
const rectangle = entry.rectangle
|
|
109
|
+
if (x > 0 && x + rectangle.width > targetWidth) {
|
|
110
|
+
y += shelfHeight + gap
|
|
111
|
+
x = 0
|
|
112
|
+
shelfHeight = 0
|
|
113
|
+
}
|
|
114
|
+
placements[entry.index] = { x, y, width: rectangle.width, height: rectangle.height }
|
|
115
|
+
x += rectangle.width + gap
|
|
116
|
+
width = Math.max(width, x - gap)
|
|
117
|
+
shelfHeight = Math.max(shelfHeight, rectangle.height)
|
|
118
|
+
}
|
|
119
|
+
return { placements, width, height: y + shelfHeight }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The aspect ratio of a box, as a number a reader can compare against the page's. */
|
|
123
|
+
export function aspectOf(width: number, height: number): number {
|
|
124
|
+
if (width <= 0 || height <= 0) return 1
|
|
125
|
+
return Math.max(width, height) / Math.min(width, height)
|
|
126
|
+
}
|