kmap-term-tree 0.9.21 → 0.9.22
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/demo/index.html +124 -10
- package/dist/src/KmapTermTree.d.ts +29 -46
- package/dist/src/KmapTermTree.js +536 -221
- package/dist/src/KmapTermTree.js.map +1 -1
- package/dist/src/asciimath-parser.d.ts +17 -0
- package/dist/src/asciimath-parser.js +472 -0
- package/dist/src/asciimath-parser.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/katex-css.d.ts +2 -0
- package/dist/src/katex-css.js +7 -0
- package/dist/src/katex-css.js.map +1 -0
- package/dist/src/model.d.ts +30 -0
- package/dist/src/model.js +26 -0
- package/dist/src/model.js.map +1 -0
- package/dist/src/parser.js.map +1 -1
- package/dist/src/tokenizer.js.map +1 -1
- package/dist/stories/index.stories.d.ts +20 -0
- package/dist/stories/index.stories.js +26 -2
- package/dist/stories/index.stories.js.map +1 -1
- package/dist/test/asciimath-parser.test.d.ts +1 -0
- package/dist/test/asciimath-parser.test.js +45 -0
- package/dist/test/asciimath-parser.test.js.map +1 -0
- package/dist/test/kmap-term-tree.test.js +30 -7
- package/dist/test/kmap-term-tree.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -50
- package/src/KmapTermTree.ts +557 -235
- package/src/asciimath-parser.ts +554 -0
- package/src/index.ts +2 -0
- package/src/katex-css.ts +7 -0
- package/src/model.ts +57 -0
- package/stories/index.stories.ts +35 -2
- package/test/asciimath-parser.test.ts +50 -0
- package/test/kmap-term-tree.test.ts +44 -7
package/src/KmapTermTree.ts
CHANGED
|
@@ -1,279 +1,601 @@
|
|
|
1
|
-
import {css, html, LitElement, PropertyValues, svg} from 'lit';
|
|
2
|
-
import {property, state, query} from 'lit/decorators.js';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
from: number,
|
|
9
|
-
to: number,
|
|
10
|
-
}
|
|
11
|
-
interface Position {
|
|
12
|
-
x: number,
|
|
13
|
-
y: number,
|
|
14
|
-
width: number,
|
|
15
|
-
height: number,
|
|
16
|
-
}
|
|
17
|
-
interface Edge {
|
|
18
|
-
from: Position,
|
|
19
|
-
to: Position,
|
|
20
|
-
}
|
|
1
|
+
import { css, html, LitElement, PropertyValues, svg } from 'lit';
|
|
2
|
+
import { property, state, query } from 'lit/decorators.js';
|
|
3
|
+
import katex from 'katex';
|
|
4
|
+
import { katexStyles } from './katex-css.js';
|
|
5
|
+
import { TermNode, TreeEdge } from './model.js';
|
|
6
|
+
import { parseAsciiMath, astToAnnotatedLatex } from './asciimath-parser.js';
|
|
7
|
+
|
|
21
8
|
export class KmapTermTree extends LitElement {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
white-space: nowrap;
|
|
39
|
-
height: 0px;
|
|
40
|
-
}
|
|
41
|
-
.notokens div {
|
|
42
|
-
display: inline-block;
|
|
43
|
-
}
|
|
44
|
-
kmap-term-tree-edges {
|
|
45
|
-
width: 100%;
|
|
46
|
-
height: 100%;
|
|
47
|
-
}
|
|
48
|
-
.token {
|
|
49
|
-
display: inline-block;
|
|
50
|
-
}
|
|
51
|
-
.token, .node {
|
|
52
|
-
display: inline-block;
|
|
53
|
-
position: relative;
|
|
54
|
-
background-color: var(--kmap-term-tree-background-color, white);
|
|
55
|
-
}
|
|
56
|
-
.node {
|
|
57
|
-
display: inline-block;
|
|
58
|
-
position: relative;
|
|
59
|
-
border: 1px solid var(--kmap-term-tree-border-color, #005b9f);
|
|
60
|
-
padding: 0px 4px;
|
|
61
|
-
border-radius: 1em;
|
|
62
|
-
margin: -1px -5px;
|
|
63
|
-
}
|
|
64
|
-
.node[depth="0"], .node:not([depth]) {
|
|
65
|
-
visibility: hidden;
|
|
66
|
-
}
|
|
67
|
-
.node[depth="1"] {
|
|
68
|
-
top: calc(var(--kmap-term-tree-vertical-distance, 1.5em) * 1.5);
|
|
69
|
-
}
|
|
70
|
-
.node[depth="2"] {
|
|
71
|
-
top: calc(var(--kmap-term-tree-vertical-distance, 1.5em) * 3);
|
|
72
|
-
}
|
|
73
|
-
.node[depth="3"] {
|
|
74
|
-
top: calc(var(--kmap-term-tree-vertical-distance, 1.5em) * 4.5);
|
|
75
|
-
}
|
|
76
|
-
.node[depth="4"] {
|
|
77
|
-
top: calc(var(--kmap-term-tree-vertical-distance, 1.5em) * 6);
|
|
78
|
-
}
|
|
79
|
-
.node[depth="5"] {
|
|
80
|
-
top: calc(var(--kmap-term-tree-vertical-distance, 1.5em) * 7.5);
|
|
81
|
-
}
|
|
82
|
-
.node[depth="6"] {
|
|
83
|
-
top: calc(var(--kmap-term-tree-vertical-distance, 1.5em) * 9);
|
|
84
|
-
}
|
|
85
|
-
`;
|
|
9
|
+
static styles = [
|
|
10
|
+
katexStyles,
|
|
11
|
+
css`
|
|
12
|
+
:host {
|
|
13
|
+
display: block;
|
|
14
|
+
position: relative;
|
|
15
|
+
font-family: var(
|
|
16
|
+
--kmap-term-tree-font-family,
|
|
17
|
+
system-ui,
|
|
18
|
+
-apple-system,
|
|
19
|
+
sans-serif
|
|
20
|
+
);
|
|
21
|
+
color: var(--kmap-term-tree-text-color, #1f2937);
|
|
22
|
+
user-select: none;
|
|
23
|
+
width: 100%;
|
|
24
|
+
}
|
|
86
25
|
|
|
87
|
-
|
|
26
|
+
.container {
|
|
27
|
+
position: relative;
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
align-items: center;
|
|
31
|
+
width: 100%;
|
|
32
|
+
box-sizing: border-box;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.tree-canvas {
|
|
36
|
+
position: relative;
|
|
37
|
+
width: 100%;
|
|
38
|
+
min-height: 80px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.formula-wrapper {
|
|
42
|
+
position: relative;
|
|
43
|
+
z-index: 2;
|
|
44
|
+
display: flex;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
align-items: center;
|
|
47
|
+
padding-top: 4px;
|
|
48
|
+
font-size: var(--kmap-term-tree-formula-font-size, 1.35rem);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.highlight-overlay {
|
|
52
|
+
position: absolute;
|
|
53
|
+
z-index: 1;
|
|
54
|
+
pointer-events: none;
|
|
55
|
+
background-color: var(
|
|
56
|
+
--kmap-term-tree-highlight-color,
|
|
57
|
+
rgba(251, 191, 36, 0.28)
|
|
58
|
+
);
|
|
59
|
+
border: 2px solid
|
|
60
|
+
var(--kmap-term-tree-highlight-outline, rgba(245, 158, 11, 0.75));
|
|
61
|
+
border-radius: 6px;
|
|
62
|
+
box-sizing: border-box;
|
|
63
|
+
transition: left 0.12s ease-out, top 0.12s ease-out,
|
|
64
|
+
width 0.12s ease-out, height 0.12s ease-out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.edges-svg {
|
|
68
|
+
position: absolute;
|
|
69
|
+
top: 0;
|
|
70
|
+
left: 0;
|
|
71
|
+
width: 100%;
|
|
72
|
+
height: 100%;
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
z-index: 1;
|
|
75
|
+
overflow: visible;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.edge-path {
|
|
79
|
+
fill: none;
|
|
80
|
+
stroke: var(--kmap-term-tree-edge-color, #f59e0b);
|
|
81
|
+
stroke-width: 2.2;
|
|
82
|
+
stroke-linecap: round;
|
|
83
|
+
opacity: 0.85;
|
|
84
|
+
transition: stroke 0.2s ease, stroke-width 0.2s ease, opacity 0.2s ease;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.edge-path.highlighted {
|
|
88
|
+
stroke: var(--kmap-term-tree-highlight-edge-color, #d97706);
|
|
89
|
+
stroke-width: 3.2;
|
|
90
|
+
opacity: 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.tree-node {
|
|
94
|
+
position: absolute;
|
|
95
|
+
z-index: 2;
|
|
96
|
+
transform: translate(-50%, -50%);
|
|
97
|
+
display: inline-flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
align-items: center;
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.tree-node:hover,
|
|
105
|
+
.tree-node.active {
|
|
106
|
+
transform: translate(-50%, -50%) scale(1.15);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.node-badge {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: center;
|
|
113
|
+
width: 28px;
|
|
114
|
+
height: 28px;
|
|
115
|
+
border-radius: 50%;
|
|
116
|
+
background: var(--kmap-term-tree-background-color, #ffffff);
|
|
117
|
+
border: 2px solid var(--kmap-term-tree-border-color, #0284c7);
|
|
118
|
+
color: var(--kmap-term-tree-node-text-color, #0284c7);
|
|
119
|
+
font-weight: 700;
|
|
120
|
+
font-size: 1rem;
|
|
121
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
|
122
|
+
box-sizing: border-box;
|
|
123
|
+
transition: all 0.2s ease;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.tree-node:hover .node-badge,
|
|
127
|
+
.tree-node.active .node-badge {
|
|
128
|
+
background: var(--kmap-term-tree-active-node-bg, #0284c7);
|
|
129
|
+
color: #ffffff;
|
|
130
|
+
border-color: var(--kmap-term-tree-active-node-border, #0284c7);
|
|
131
|
+
box-shadow: 0 4px 8px rgba(2, 132, 199, 0.35);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.node-label {
|
|
135
|
+
margin-top: 2px;
|
|
136
|
+
font-size: 0.7rem;
|
|
137
|
+
font-weight: 500;
|
|
138
|
+
color: #64748b;
|
|
139
|
+
white-space: nowrap;
|
|
140
|
+
background: rgba(255, 255, 255, 0.95);
|
|
141
|
+
padding: 1px 4px;
|
|
142
|
+
border-radius: 4px;
|
|
143
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* KaTeX Highlight */
|
|
147
|
+
.highlight-term {
|
|
148
|
+
background-color: var(
|
|
149
|
+
--kmap-term-tree-highlight-color,
|
|
150
|
+
rgba(251, 191, 36, 0.35)
|
|
151
|
+
) !important;
|
|
152
|
+
border-radius: 4px;
|
|
153
|
+
outline: 2px solid
|
|
154
|
+
var(--kmap-term-tree-highlight-outline, rgba(245, 158, 11, 0.7));
|
|
155
|
+
transition: background-color 0.2s ease, outline 0.2s ease;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.info-bar {
|
|
159
|
+
margin-top: 12px;
|
|
160
|
+
padding: 6px 14px;
|
|
161
|
+
background: #f8fafc;
|
|
162
|
+
border: 1px solid #e2e8f0;
|
|
163
|
+
border-radius: 8px;
|
|
164
|
+
font-size: 0.88rem;
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: 8px;
|
|
168
|
+
color: #334155;
|
|
169
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.info-badge {
|
|
173
|
+
font-weight: 700;
|
|
174
|
+
color: #0284c7;
|
|
175
|
+
background: #e0f2fe;
|
|
176
|
+
padding: 2px 8px;
|
|
177
|
+
border-radius: 4px;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.error-msg {
|
|
181
|
+
color: #dc2626;
|
|
182
|
+
padding: 8px;
|
|
183
|
+
font-size: 0.9rem;
|
|
184
|
+
}
|
|
185
|
+
`,
|
|
186
|
+
];
|
|
88
187
|
|
|
89
188
|
@property({ type: String }) term?: string;
|
|
90
|
-
@property({type: Number}) tension: number = 0;
|
|
189
|
+
@property({ type: Number }) tension: number = 0.5;
|
|
190
|
+
@property({ type: Boolean }) showLabels: boolean = false;
|
|
191
|
+
@property({ type: Boolean }) showDetails: boolean = true;
|
|
91
192
|
|
|
92
|
-
@state()
|
|
93
|
-
@state()
|
|
94
|
-
@state()
|
|
95
|
-
@state()
|
|
96
|
-
@state()
|
|
193
|
+
@state() private ast?: TermNode;
|
|
194
|
+
@state() private error?: string;
|
|
195
|
+
@state() private flatNodes: TermNode[] = [];
|
|
196
|
+
@state() private edges: TreeEdge[] = [];
|
|
197
|
+
@state() private activeNodeId?: string;
|
|
198
|
+
@state() private activeSubtreeIds: Set<string> = new Set();
|
|
199
|
+
@state() private treeHeight: number = 100;
|
|
200
|
+
@state() private highlightRect?: {
|
|
201
|
+
left: number;
|
|
202
|
+
top: number;
|
|
203
|
+
width: number;
|
|
204
|
+
height: number;
|
|
205
|
+
};
|
|
97
206
|
|
|
98
|
-
@
|
|
207
|
+
@query('#formula') private formulaElement?: HTMLElement;
|
|
208
|
+
@query('.tree-canvas') private treeCanvasElement?: HTMLElement;
|
|
99
209
|
|
|
100
|
-
|
|
101
|
-
@query("#tokens") tokensElement?: HTMLDivElement;
|
|
210
|
+
private resizeObserver?: ResizeObserver;
|
|
102
211
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
212
|
+
connectedCallback() {
|
|
213
|
+
super.connectedCallback();
|
|
214
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
215
|
+
this.recalculateLayout();
|
|
216
|
+
});
|
|
107
217
|
}
|
|
108
218
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
});
|
|
125
|
-
this.depths = map;
|
|
126
|
-
this.connections = connections;
|
|
127
|
-
}
|
|
219
|
+
disconnectedCallback() {
|
|
220
|
+
this.resizeObserver?.disconnect();
|
|
221
|
+
super.disconnectedCallback();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Support slotted content: <kmap-term-tree>2a+3b</kmap-term-tree>
|
|
225
|
+
updateSlotted(event: Event) {
|
|
226
|
+
const slot = event.target as HTMLSlotElement;
|
|
227
|
+
const text = slot
|
|
228
|
+
.assignedNodes()
|
|
229
|
+
.map(n => n.textContent)
|
|
230
|
+
.join('')
|
|
231
|
+
.trim();
|
|
232
|
+
if (text && text !== this.term) {
|
|
233
|
+
this.term = text;
|
|
128
234
|
}
|
|
129
|
-
super.update(_changedProperties);
|
|
130
235
|
}
|
|
131
236
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (node.rightChildNode)
|
|
137
|
-
depth = Math.max(depth, this.calcDepths(node.rightChildNode, map) + 1);
|
|
138
|
-
map[node.token.id] = depth;
|
|
139
|
-
return depth;
|
|
237
|
+
protected willUpdate(changedProperties: PropertyValues) {
|
|
238
|
+
if (changedProperties.has('term') && this.term !== undefined) {
|
|
239
|
+
this.parseInput(this.term);
|
|
240
|
+
}
|
|
140
241
|
}
|
|
141
242
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
<div class="node" id="n${t.id}" depth="${ifDefined(this.depths ? this.depths[t.id]: undefined)}">${KmapTermTree._prettify(t.value)}</div>
|
|
163
|
-
`)}
|
|
164
|
-
</div>
|
|
165
|
-
` : ''}
|
|
166
|
-
<div hidden>
|
|
167
|
-
<slot @slotchange=${this.updateSlotted}></slot>
|
|
168
|
-
</div>
|
|
169
|
-
`;
|
|
243
|
+
private parseInput(termStr: string) {
|
|
244
|
+
try {
|
|
245
|
+
this.error = undefined;
|
|
246
|
+
const cleanTerm = termStr.trim();
|
|
247
|
+
if (!cleanTerm) {
|
|
248
|
+
this.ast = undefined;
|
|
249
|
+
this.flatNodes = [];
|
|
250
|
+
this.edges = [];
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.ast = parseAsciiMath(cleanTerm);
|
|
255
|
+
this.activeNodeId = this.ast.id;
|
|
256
|
+
this.activeSubtreeIds = this.collectSubtreeIds(this.ast);
|
|
257
|
+
} catch (e: any) {
|
|
258
|
+
this.error = e.message || 'Fehler beim Parsen des Terms';
|
|
259
|
+
this.ast = undefined;
|
|
260
|
+
this.flatNodes = [];
|
|
261
|
+
this.edges = [];
|
|
262
|
+
}
|
|
170
263
|
}
|
|
171
264
|
|
|
172
|
-
protected async updated(
|
|
173
|
-
if (
|
|
265
|
+
protected async updated(changedProperties: PropertyValues) {
|
|
266
|
+
if (changedProperties.has('term') || changedProperties.has('ast')) {
|
|
267
|
+
this.renderKaTeX();
|
|
174
268
|
await this.updateComplete;
|
|
269
|
+
this.recalculateLayout();
|
|
270
|
+
if (this.treeCanvasElement && this.resizeObserver) {
|
|
271
|
+
this.resizeObserver.observe(this.treeCanvasElement);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private renderKaTeX() {
|
|
277
|
+
if (!this.formulaElement || !this.ast) return;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const latex = astToAnnotatedLatex(this.ast);
|
|
281
|
+
katex.render(latex, this.formulaElement, {
|
|
282
|
+
trust: true,
|
|
283
|
+
strict: false,
|
|
284
|
+
throwOnError: false,
|
|
285
|
+
});
|
|
286
|
+
} catch (err: any) {
|
|
287
|
+
this.error = err.message;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private recalculateLayout() {
|
|
292
|
+
if (
|
|
293
|
+
!this.ast ||
|
|
294
|
+
!this.formulaElement ||
|
|
295
|
+
!this.treeCanvasElement ||
|
|
296
|
+
!this.shadowRoot
|
|
297
|
+
)
|
|
298
|
+
return;
|
|
299
|
+
|
|
300
|
+
const canvasRect = this.treeCanvasElement.getBoundingClientRect();
|
|
301
|
+
if (canvasRect.width === 0) return;
|
|
302
|
+
|
|
303
|
+
const flatNodes: TermNode[] = [];
|
|
304
|
+
const nodeMap = new Map<string, TermNode>();
|
|
305
|
+
|
|
306
|
+
// 1. Collect all nodes and measure leaf positions from KaTeX DOM elements
|
|
307
|
+
const collectAndPositionLeaves = (node: TermNode) => {
|
|
308
|
+
nodeMap.set(node.id, node);
|
|
309
|
+
|
|
310
|
+
if (node.children.length === 0) {
|
|
311
|
+
// Leaf node: measure rendered KaTeX span
|
|
312
|
+
const el = this.shadowRoot!.getElementById(node.id);
|
|
313
|
+
if (el) {
|
|
314
|
+
const rect = el.getBoundingClientRect();
|
|
315
|
+
node.x = rect.left - canvasRect.left + rect.width / 2;
|
|
316
|
+
node.y = rect.bottom - canvasRect.top;
|
|
317
|
+
node.width = rect.width;
|
|
318
|
+
node.height = rect.height;
|
|
319
|
+
} else {
|
|
320
|
+
node.x = canvasRect.width / 2;
|
|
321
|
+
node.y = 20;
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
flatNodes.push(node);
|
|
325
|
+
for (const child of node.children) {
|
|
326
|
+
collectAndPositionLeaves(child);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
175
330
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
331
|
+
collectAndPositionLeaves(this.ast);
|
|
332
|
+
|
|
333
|
+
// 2. Position inner nodes bottom-up based on children coordinates
|
|
334
|
+
const formulaRect = this.formulaElement.getBoundingClientRect();
|
|
335
|
+
const formulaBottomRel = formulaRect.bottom - canvasRect.top;
|
|
336
|
+
const initialGap = 34; // distance from formula bottom to level 1 operators
|
|
337
|
+
const rowHeight = 40; // distance between tree levels
|
|
338
|
+
|
|
339
|
+
const positionInnerNodes = (node: TermNode) => {
|
|
340
|
+
for (const child of node.children) {
|
|
341
|
+
if (child.children.length > 0) {
|
|
342
|
+
positionInnerNodes(child);
|
|
343
|
+
}
|
|
184
344
|
}
|
|
185
345
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
346
|
+
if (node.children.length > 0) {
|
|
347
|
+
if (node.type === 'function' || node.isUnary) {
|
|
348
|
+
// Center under the full function / unary expression: e.g. sin(arg) or -arg
|
|
349
|
+
const el = this.shadowRoot!.getElementById(node.id);
|
|
350
|
+
if (el) {
|
|
351
|
+
const rect = el.getBoundingClientRect();
|
|
352
|
+
node.x = rect.left - canvasRect.left + rect.width / 2;
|
|
353
|
+
} else {
|
|
354
|
+
node.x = node.children[0].x ?? 0;
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
const first = node.children[0];
|
|
358
|
+
const last = node.children[node.children.length - 1];
|
|
359
|
+
node.x = ((first.x ?? 0) + (last.x ?? 0)) / 2;
|
|
192
360
|
}
|
|
193
|
-
|
|
361
|
+
node.y = formulaBottomRel + initialGap + (node.depth - 1) * rowHeight;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
positionInnerNodes(this.ast);
|
|
366
|
+
|
|
367
|
+
// 3. Build edges between parent nodes and their children
|
|
368
|
+
const edges: TreeEdge[] = [];
|
|
369
|
+
for (const node of flatNodes) {
|
|
370
|
+
if (node.x === undefined || node.y === undefined) continue;
|
|
371
|
+
|
|
372
|
+
for (const child of node.children) {
|
|
373
|
+
if (child.x === undefined || child.y === undefined) continue;
|
|
374
|
+
|
|
375
|
+
edges.push({
|
|
376
|
+
fromNodeId: child.id,
|
|
377
|
+
toNodeId: node.id,
|
|
378
|
+
fromX: child.x,
|
|
379
|
+
fromY: child.y + (child.depth > 0 ? 14 : 2),
|
|
380
|
+
toX: node.x,
|
|
381
|
+
toY: node.y - 14,
|
|
382
|
+
});
|
|
194
383
|
}
|
|
195
384
|
}
|
|
385
|
+
|
|
386
|
+
const maxDepth = this.ast.depth;
|
|
387
|
+
this.treeHeight = Math.max(
|
|
388
|
+
60,
|
|
389
|
+
formulaBottomRel + initialGap + (maxDepth - 1) * rowHeight + 24
|
|
390
|
+
);
|
|
391
|
+
this.flatNodes = flatNodes;
|
|
392
|
+
this.edges = edges;
|
|
393
|
+
this.updateHighlightRect(this.activeNodeId);
|
|
394
|
+
this.requestUpdate();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private collectSubtreeIds(node: TermNode): Set<string> {
|
|
398
|
+
const ids = new Set<string>();
|
|
399
|
+
const traverse = (n: TermNode) => {
|
|
400
|
+
ids.add(n.id);
|
|
401
|
+
for (const c of n.children) traverse(c);
|
|
402
|
+
};
|
|
403
|
+
traverse(node);
|
|
404
|
+
return ids;
|
|
196
405
|
}
|
|
197
406
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
407
|
+
private onNodeHover(node: TermNode, enter: boolean) {
|
|
408
|
+
if (!enter) {
|
|
409
|
+
// Revert highlight to root node
|
|
410
|
+
if (this.ast) {
|
|
411
|
+
this.setActiveNode(this.ast);
|
|
412
|
+
}
|
|
202
413
|
return;
|
|
203
|
-
|
|
204
|
-
|
|
414
|
+
}
|
|
415
|
+
this.setActiveNode(node);
|
|
416
|
+
}
|
|
205
417
|
|
|
206
|
-
|
|
418
|
+
private setActiveNode(node: TermNode) {
|
|
419
|
+
this.activeNodeId = node.id;
|
|
420
|
+
this.activeSubtreeIds = this.collectSubtreeIds(node);
|
|
421
|
+
this.updateHighlightRect(node.id);
|
|
422
|
+
this.requestUpdate();
|
|
207
423
|
}
|
|
208
424
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
425
|
+
private findNode(
|
|
426
|
+
root: TermNode | undefined,
|
|
427
|
+
id: string
|
|
428
|
+
): TermNode | undefined {
|
|
429
|
+
if (!root) return undefined;
|
|
430
|
+
if (root.id === id) return root;
|
|
431
|
+
for (const child of root.children) {
|
|
432
|
+
const found = this.findNode(child, id);
|
|
433
|
+
if (found) return found;
|
|
216
434
|
}
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private getNodeVisualBounds(
|
|
439
|
+
node: TermNode
|
|
440
|
+
): { top: number; bottom: number; left: number; right: number } | null {
|
|
441
|
+
if (!this.shadowRoot) return null;
|
|
442
|
+
|
|
443
|
+
let minTop = Infinity;
|
|
444
|
+
let maxBottom = -Infinity;
|
|
445
|
+
let minLeft = Infinity;
|
|
446
|
+
let maxRight = -Infinity;
|
|
447
|
+
|
|
448
|
+
const collectBounds = (n: TermNode) => {
|
|
449
|
+
const el = this.shadowRoot!.getElementById(n.id);
|
|
450
|
+
if (el) {
|
|
451
|
+
const r = el.getBoundingClientRect();
|
|
452
|
+
if (r.width > 0 && r.height > 0) {
|
|
453
|
+
minTop = Math.min(minTop, r.top);
|
|
454
|
+
maxBottom = Math.max(maxBottom, r.bottom);
|
|
455
|
+
minLeft = Math.min(minLeft, r.left);
|
|
456
|
+
maxRight = Math.max(maxRight, r.right);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
for (const child of n.children) {
|
|
460
|
+
collectBounds(child);
|
|
461
|
+
}
|
|
222
462
|
};
|
|
463
|
+
|
|
464
|
+
collectBounds(node);
|
|
465
|
+
|
|
466
|
+
if (minTop === Infinity) return null;
|
|
467
|
+
return { top: minTop, bottom: maxBottom, left: minLeft, right: maxRight };
|
|
223
468
|
}
|
|
224
469
|
|
|
225
|
-
private
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
case '-':
|
|
230
|
-
return "−"; // math minus
|
|
231
|
-
default:
|
|
232
|
-
return value;
|
|
470
|
+
private updateHighlightRect(nodeId?: string) {
|
|
471
|
+
if (!nodeId || !this.shadowRoot || !this.treeCanvasElement || !this.ast) {
|
|
472
|
+
this.highlightRect = undefined;
|
|
473
|
+
return;
|
|
233
474
|
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
475
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
stroke: var(--kmap-term-tree-edge-color, #fbc02d);
|
|
242
|
-
position: relative;
|
|
476
|
+
const node = this.findNode(this.ast, nodeId);
|
|
477
|
+
if (!node) {
|
|
478
|
+
this.highlightRect = undefined;
|
|
479
|
+
return;
|
|
243
480
|
}
|
|
244
|
-
`;
|
|
245
481
|
|
|
246
|
-
|
|
247
|
-
|
|
482
|
+
const bounds = this.getNodeVisualBounds(node);
|
|
483
|
+
if (!bounds) {
|
|
484
|
+
this.highlightRect = undefined;
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
248
487
|
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
488
|
+
const canvasRect = this.treeCanvasElement.getBoundingClientRect();
|
|
489
|
+
const paddingX = 4;
|
|
490
|
+
const paddingY = 4;
|
|
491
|
+
|
|
492
|
+
this.highlightRect = {
|
|
493
|
+
left: bounds.left - canvasRect.left - paddingX,
|
|
494
|
+
top: bounds.top - canvasRect.top - paddingY,
|
|
495
|
+
width: bounds.right - bounds.left + paddingX * 2,
|
|
496
|
+
height: bounds.bottom - bounds.top + paddingY * 2,
|
|
497
|
+
};
|
|
259
498
|
}
|
|
260
499
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
var hy1 = fromy + delta;
|
|
270
|
-
var hx2 = tox;
|
|
271
|
-
var hy2 = toy - delta;
|
|
272
|
-
return "M " + fromx + " " + fromy +
|
|
273
|
-
" C " + hx1 + " " + hy1
|
|
274
|
-
+ " " + hx2 + " " + hy2
|
|
275
|
-
+ " " + tox + " " + toy;
|
|
500
|
+
private renderPath(edge: TreeEdge): string {
|
|
501
|
+
const delta = (edge.toY - edge.fromY) * this.tension;
|
|
502
|
+
const hx1 = edge.fromX;
|
|
503
|
+
const hy1 = edge.fromY + delta;
|
|
504
|
+
const hx2 = edge.toX;
|
|
505
|
+
const hy2 = edge.toY - delta;
|
|
506
|
+
|
|
507
|
+
return `M ${edge.fromX} ${edge.fromY} C ${hx1} ${hy1} ${hx2} ${hy2} ${edge.toX} ${edge.toY}`;
|
|
276
508
|
}
|
|
277
509
|
|
|
510
|
+
render() {
|
|
511
|
+
const activeNode =
|
|
512
|
+
this.flatNodes.find(n => n.id === this.activeNodeId) || this.ast;
|
|
513
|
+
const isRoot = activeNode?.id === this.ast?.id;
|
|
514
|
+
|
|
515
|
+
return html`
|
|
516
|
+
<div class="container">
|
|
517
|
+
<!-- Canvas area with formula, edges, and operator nodes sharing the same coordinate space -->
|
|
518
|
+
<div class="tree-canvas" style="height: ${this.treeHeight}px">
|
|
519
|
+
${this.highlightRect
|
|
520
|
+
? html`
|
|
521
|
+
<div
|
|
522
|
+
class="highlight-overlay"
|
|
523
|
+
style="left: ${this.highlightRect.left}px; top: ${this
|
|
524
|
+
.highlightRect.top}px; width: ${this.highlightRect
|
|
525
|
+
.width}px; height: ${this.highlightRect.height}px;"
|
|
526
|
+
></div>
|
|
527
|
+
`
|
|
528
|
+
: ''}
|
|
529
|
+
|
|
530
|
+
<div class="formula-wrapper">
|
|
531
|
+
<div id="formula"></div>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
${this.error
|
|
535
|
+
? html`<div class="error-msg">${this.error}</div>`
|
|
536
|
+
: html`
|
|
537
|
+
<svg class="edges-svg">
|
|
538
|
+
${this.edges.map(
|
|
539
|
+
edge => svg`
|
|
540
|
+
<path
|
|
541
|
+
class="edge-path ${
|
|
542
|
+
this.activeSubtreeIds.has(edge.fromNodeId) &&
|
|
543
|
+
this.activeSubtreeIds.has(edge.toNodeId)
|
|
544
|
+
? 'highlighted'
|
|
545
|
+
: ''
|
|
546
|
+
}"
|
|
547
|
+
d="${this.renderPath(edge)}"
|
|
548
|
+
/>
|
|
549
|
+
`
|
|
550
|
+
)}
|
|
551
|
+
</svg>
|
|
552
|
+
|
|
553
|
+
${this.flatNodes.map(
|
|
554
|
+
node => html`
|
|
555
|
+
<div
|
|
556
|
+
class="tree-node ${this.activeNodeId === node.id
|
|
557
|
+
? 'active'
|
|
558
|
+
: ''}"
|
|
559
|
+
style="left: ${node.x}px; top: ${node.y}px;"
|
|
560
|
+
@mouseenter=${() => this.onNodeHover(node, true)}
|
|
561
|
+
@mouseleave=${() => this.onNodeHover(node, false)}
|
|
562
|
+
@click=${() => this.setActiveNode(node)}
|
|
563
|
+
>
|
|
564
|
+
<div class="node-badge" title="${node.label}">
|
|
565
|
+
${node.symbol}
|
|
566
|
+
</div>
|
|
567
|
+
${this.showLabels
|
|
568
|
+
? html`<span class="node-label">${node.label}</span>`
|
|
569
|
+
: ''}
|
|
570
|
+
</div>
|
|
571
|
+
`
|
|
572
|
+
)}
|
|
573
|
+
`}
|
|
574
|
+
</div>
|
|
575
|
+
|
|
576
|
+
<!-- Didactic info box -->
|
|
577
|
+
${this.showDetails && activeNode
|
|
578
|
+
? html`
|
|
579
|
+
<div class="info-bar">
|
|
580
|
+
<span>${isRoot ? 'Hauptrechenart:' : 'Teilterm:'}</span>
|
|
581
|
+
<span class="info-badge">${activeNode.label}</span>
|
|
582
|
+
<span style="color: #64748b">
|
|
583
|
+
${activeNode.children.length === 2
|
|
584
|
+
? `(verknüpft 2 Teilterme mit ${activeNode.symbol})`
|
|
585
|
+
: activeNode.isUnary
|
|
586
|
+
? `(wirkt auf ${
|
|
587
|
+
activeNode.children[0].label || 'Ausdruck'
|
|
588
|
+
})`
|
|
589
|
+
: ''}
|
|
590
|
+
</span>
|
|
591
|
+
</div>
|
|
592
|
+
`
|
|
593
|
+
: ''}
|
|
594
|
+
|
|
595
|
+
<div hidden>
|
|
596
|
+
<slot @slotchange=${this.updateSlotted}></slot>
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
`;
|
|
600
|
+
}
|
|
278
601
|
}
|
|
279
|
-
window.customElements.define('kmap-term-tree-edges', KmapTermTreeEdge);
|