linny-r 1.6.3 → 1.6.5
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/package.json +1 -1
- package/static/index.html +17 -0
- package/static/linny-r.css +46 -2
- package/static/scripts/linny-r-gui-constraint-editor.js +154 -29
- package/static/scripts/linny-r-gui-controller.js +3 -3
- package/static/scripts/linny-r-gui-file-manager.js +22 -8
- package/static/scripts/linny-r-gui-paper.js +28 -15
- package/static/scripts/linny-r-model.js +2 -2
- package/static/scripts/linny-r-vm.js +4 -4
package/package.json
CHANGED
package/static/index.html
CHANGED
@@ -1300,6 +1300,23 @@ NOTE: Unit symbols are case-sensitive, so BTU ≠ Btu">
|
|
1300
1300
|
</div>
|
1301
1301
|
</div>
|
1302
1302
|
|
1303
|
+
<!-- BOUNDLINE POINT modal -->
|
1304
|
+
<div id="boundline-point-modal" class="modal">
|
1305
|
+
<div id="boundline-point-dlg" class="inp-dlg">
|
1306
|
+
<div class="dlg-title">
|
1307
|
+
Precise point coordinates
|
1308
|
+
<img class="cancel-btn" src="images/cancel.png">
|
1309
|
+
<img class="ok-btn" src="images/ok.png">
|
1310
|
+
</div>
|
1311
|
+
<div id="boundline-point-x-lbl">X =</div>
|
1312
|
+
<input id="boundline-point-x" type="text" autocomplete="off">
|
1313
|
+
<div id="boundline-point-x-percent">%</div>
|
1314
|
+
<div id="boundline-point-y-lbl">Y =</div>
|
1315
|
+
<input id="boundline-point-y" type="text" autocomplete="off">
|
1316
|
+
<div id="boundline-point-y-percent">%</div>
|
1317
|
+
</div>
|
1318
|
+
</div>
|
1319
|
+
|
1303
1320
|
<!-- PROCESS properties dialog -->
|
1304
1321
|
<div id="process-modal" class="modal">
|
1305
1322
|
<div id="process-dlg" class="inp-dlg">
|
package/static/linny-r.css
CHANGED
@@ -1922,8 +1922,8 @@ div.menu-item:hover {
|
|
1922
1922
|
#constraint-point {
|
1923
1923
|
position: absolute;
|
1924
1924
|
top: 393px;
|
1925
|
-
left:
|
1926
|
-
width:
|
1925
|
+
left: 100px;
|
1926
|
+
width: 175px;
|
1927
1927
|
text-align: center;
|
1928
1928
|
}
|
1929
1929
|
|
@@ -2009,6 +2009,50 @@ div.menu-item:hover {
|
|
2009
2009
|
right: 0px;
|
2010
2010
|
}
|
2011
2011
|
|
2012
|
+
/* the BOUNDLINE POINT DIALOG allows entering precise coordinates */
|
2013
|
+
#boundline-point-dlg {
|
2014
|
+
width: 200px;
|
2015
|
+
height: 67px;
|
2016
|
+
}
|
2017
|
+
|
2018
|
+
#boundline-point-x-lbl {
|
2019
|
+
position: absolute;
|
2020
|
+
top: 25px;
|
2021
|
+
left: 2px;
|
2022
|
+
}
|
2023
|
+
|
2024
|
+
#boundline-point-x {
|
2025
|
+
position: absolute;
|
2026
|
+
top: 23px;
|
2027
|
+
left: 24px;
|
2028
|
+
width: 160px;
|
2029
|
+
}
|
2030
|
+
|
2031
|
+
#boundline-point-x-percent {
|
2032
|
+
position: absolute;
|
2033
|
+
top: 25px;
|
2034
|
+
left: 188px;
|
2035
|
+
}
|
2036
|
+
|
2037
|
+
#boundline-point-y-lbl {
|
2038
|
+
position: absolute;
|
2039
|
+
top: 47px;
|
2040
|
+
left: 2px;
|
2041
|
+
}
|
2042
|
+
|
2043
|
+
#boundline-point-y {
|
2044
|
+
position: absolute;
|
2045
|
+
top: 45px;
|
2046
|
+
left: 24px;
|
2047
|
+
width: 160px;
|
2048
|
+
}
|
2049
|
+
|
2050
|
+
#boundline-point-y-percent {
|
2051
|
+
position: absolute;
|
2052
|
+
top: 47px;
|
2053
|
+
left: 188px;
|
2054
|
+
}
|
2055
|
+
|
2012
2056
|
/* NOTE: The following dialogs are not modal, but "stay-on-top",
|
2013
2057
|
i.e., windows that remain open (until dismissed) and above
|
2014
2058
|
the main window, and they are draggable and resizable.
|
@@ -56,7 +56,7 @@ class ConstraintEditor {
|
|
56
56
|
this.container.addEventListener('mousemove',
|
57
57
|
(event) => CONSTRAINT_EDITOR.mouseMove(event));
|
58
58
|
this.container.addEventListener('mousedown',
|
59
|
-
() => CONSTRAINT_EDITOR.mouseDown());
|
59
|
+
() => CONSTRAINT_EDITOR.mouseDown(event));
|
60
60
|
this.container.addEventListener('mouseup',
|
61
61
|
() => CONSTRAINT_EDITOR.mouseUp());
|
62
62
|
// NOTE: interpret leaving the area as a mouse-up so that dragging ceases
|
@@ -84,6 +84,11 @@ class ConstraintEditor {
|
|
84
84
|
this.delete_bl_btn = document.getElementById('del-bl-btn');
|
85
85
|
this.delete_bl_btn.addEventListener('click',
|
86
86
|
() => CONSTRAINT_EDITOR.deleteBoundLine());
|
87
|
+
this.point_modal = new ModalDialog('boundline-point');
|
88
|
+
this.point_modal.ok.addEventListener(
|
89
|
+
'click', () => CONSTRAINT_EDITOR.setPointPosition());
|
90
|
+
this.point_modal.cancel.addEventListener(
|
91
|
+
'click', () => CONSTRAINT_EDITOR.point_modal.hide());
|
87
92
|
// The chart is stored as an SVG string
|
88
93
|
this.svg = '';
|
89
94
|
// Scale, origin X and Y assume a 300x300 px square chart area
|
@@ -93,7 +98,7 @@ class ConstraintEditor {
|
|
93
98
|
// 0 => silver, LE => orange/red, GE => cyan/blue, EQ => purple
|
94
99
|
this.line_color = ['#a0a0a0', '#c04000', '#0040c0', '#9000a0'];
|
95
100
|
// Use brighter shades if selected (darker for gray)
|
96
|
-
this.selected_color = ['#808080', '#ff8040', '#
|
101
|
+
this.selected_color = ['#808080', '#ff8040', '#00b0d0', '#a800ff'];
|
97
102
|
// The selected bound line object (NULL => no line selected)
|
98
103
|
this.selected = null;
|
99
104
|
// Cursor position in chart coordinates (100 x 100 grid)
|
@@ -105,6 +110,7 @@ class ConstraintEditor {
|
|
105
110
|
this.on_point = -1;
|
106
111
|
this.dragged_point = -1;
|
107
112
|
this.selected_point = -1;
|
113
|
+
this.last_time_clicked = 0;
|
108
114
|
this.cursor = 'default';
|
109
115
|
// Properties for tracking which constraint is being edited.
|
110
116
|
this.edited_constraint = null;
|
@@ -118,6 +124,21 @@ class ConstraintEditor {
|
|
118
124
|
// NOTE: All edits will be ignored unless the modeler clicks OK.
|
119
125
|
}
|
120
126
|
|
127
|
+
get doubleClicked() {
|
128
|
+
const
|
129
|
+
now = Date.now(),
|
130
|
+
dt = now - this.last_time_clicked;
|
131
|
+
this.last_time_clicked = now;
|
132
|
+
if(this.on_point === this.selected_point) {
|
133
|
+
// Consider click to be "double" if it occurred less than 300 ms ago
|
134
|
+
if(dt < 300) {
|
135
|
+
this.last_time_clicked = 0;
|
136
|
+
return true;
|
137
|
+
}
|
138
|
+
}
|
139
|
+
return false;
|
140
|
+
}
|
141
|
+
|
121
142
|
mouseMove(e) {
|
122
143
|
// The onMouseMove response of the constraint editor's graph area
|
123
144
|
// Calculate cursor point without restricting it to 100x100 grid
|
@@ -132,16 +153,18 @@ class ConstraintEditor {
|
|
132
153
|
this.pos_y = Math.min(100, Math.max(0, y));
|
133
154
|
this.updateStatus();
|
134
155
|
if(this.dragged_point >= 0) {
|
135
|
-
this.movePoint();
|
156
|
+
this.movePoint(this.pos_x, this.pos_y);
|
136
157
|
} else {
|
137
158
|
this.checkLines();
|
138
159
|
}
|
139
160
|
}
|
140
161
|
|
141
|
-
mouseDown() {
|
142
|
-
// The onMouseDown response of the constraint editor's graph area
|
162
|
+
mouseDown(e) {
|
163
|
+
// The onMouseDown response of the constraint editor's graph area.
|
143
164
|
if(this.adding_point) {
|
144
165
|
this.doAddPointToLine();
|
166
|
+
} else if((e.altKey || this.doubleClicked) && this.on_point >= 0) {
|
167
|
+
this.positionPoint();
|
145
168
|
} else if(this.on_line) {
|
146
169
|
this.selectBoundLine(this.on_line);
|
147
170
|
this.dragged_point = this.on_point;
|
@@ -155,7 +178,7 @@ class ConstraintEditor {
|
|
155
178
|
}
|
156
179
|
|
157
180
|
mouseUp() {
|
158
|
-
// The onMouseUp response of the constraint editor's graph area
|
181
|
+
// The onMouseUp response of the constraint editor's graph area.
|
159
182
|
this.dragged_point = -1;
|
160
183
|
this.container.style.cursor = this.cursor;
|
161
184
|
this.updateStatus();
|
@@ -179,36 +202,85 @@ class ConstraintEditor {
|
|
179
202
|
this.container.style.cursor = this.cursor;
|
180
203
|
}
|
181
204
|
|
182
|
-
arrowKey(
|
205
|
+
arrowKey(e) {
|
206
|
+
// Move point by 1 grid unit (1/3 pixel), or more precisely when
|
207
|
+
// Shift, Ctrl and/or Alt are pressed. Shift resolution is 1/10,
|
208
|
+
// Ctrl resolution = 1/100, combined => 1/1000. Just Alt resolution
|
209
|
+
// is 1/10000, and with Shift + Ctrl becomes 1e-7.
|
183
210
|
if(this.selected && this.selected_point >= 0) {
|
211
|
+
const custom = e.shiftKey || e.ctrlKey || e.altKey;
|
212
|
+
let divisor = 3;
|
213
|
+
if(e.shiftKey) {
|
214
|
+
divisor = 10;
|
215
|
+
if(e.ctrlKey) divisor = 1000;
|
216
|
+
} else if(e.ctrlKey) {
|
217
|
+
divisor = 100;
|
218
|
+
}
|
219
|
+
if(e.altKey) {
|
220
|
+
if(divisor === 3) {
|
221
|
+
divisor = 10000;
|
222
|
+
} else {
|
223
|
+
divisor *= 10000;
|
224
|
+
}
|
225
|
+
}
|
184
226
|
const
|
227
|
+
k = e.keyCode,
|
185
228
|
i = this.selected_point,
|
186
229
|
pts = this.selected.points,
|
187
230
|
li = pts.length - 1,
|
188
231
|
p = pts[this.selected_point],
|
189
232
|
minx = (i === 0 ? 0 : (i === li ? 100 : pts[i - 1][0])),
|
190
233
|
maxx = (i === 0 ? 0 : (i === li ? 100 : pts[i + 1][0]));
|
234
|
+
let cx = false,
|
235
|
+
cy = false;
|
191
236
|
if(k === 37) {
|
192
|
-
p[0] = Math.max(minx, p[0] - 1/
|
193
|
-
|
194
|
-
|
237
|
+
p[0] = Math.max(minx, p[0] - 1 / divisor);
|
238
|
+
cx = true;
|
239
|
+
} else if (k === 38 && p[1] <= 100 - 1 / divisor) {
|
240
|
+
p[1] += 1 / divisor;
|
241
|
+
cy = true;
|
195
242
|
} else if (k === 39) {
|
196
|
-
p[0] = Math.min(maxx, p[0] + 1/
|
197
|
-
|
198
|
-
|
243
|
+
p[0] = Math.min(maxx, p[0] + 1 / divisor);
|
244
|
+
cx = true;
|
245
|
+
} else if (k === 40 && p[1] >= 1 / divisor) {
|
246
|
+
p[1] -= 1 / divisor;
|
247
|
+
cy = true;
|
248
|
+
}
|
249
|
+
// NOTE: Compensate for small numerical errors
|
250
|
+
const cp = this.customPoint(p[0], p[1]);
|
251
|
+
if(cx) {
|
252
|
+
if(cp & 1 && custom) {
|
253
|
+
p[0] = Math.round(divisor * p[0]) / divisor;
|
254
|
+
} else {
|
255
|
+
p[0] = Math.round(3 * p[0]) / 3;
|
256
|
+
}
|
257
|
+
}
|
258
|
+
if(cy) {
|
259
|
+
if(cp & 2 && custom) {
|
260
|
+
p[1] = Math.round(divisor * p[1]) / divisor;
|
261
|
+
} else {
|
262
|
+
p[1] = Math.round(3 * p[1]) / 3;
|
263
|
+
}
|
199
264
|
}
|
200
|
-
// NOTE: compensate for small numerical errors
|
201
|
-
p[0] = Math.round(3 * p[0]) / 3;
|
202
|
-
p[1] = Math.round(3 * p[1]) / 3;
|
203
265
|
this.draw();
|
204
266
|
this.updateEquation();
|
205
267
|
}
|
206
268
|
}
|
207
269
|
|
270
|
+
customPoint(x, y) {
|
271
|
+
// Return 0 if `x` and `y` both are "regular" points on the pixel
|
272
|
+
// grid. For these regular points, X and Y are multiples of 1/3,
|
273
|
+
// so 3*X and 3*Y should both be integer (apart from numerical
|
274
|
+
// imprecision). If only X is custom, return 1, if only Y is custom,
|
275
|
+
// return 2, and if both are custom, return 3.
|
276
|
+
return (Math.abs(3*x - Math.round(3*x)) > VM.NEAR_ZERO ? 1 : 0) +
|
277
|
+
(Math.abs(3*y - Math.round(3*y)) > VM.NEAR_ZERO ? 2 : 0);
|
278
|
+
}
|
279
|
+
|
208
280
|
point(x, y) {
|
209
|
-
//
|
281
|
+
// Return a string denoting the point (x, y) in SVG notation, assuming
|
210
282
|
// that x and y are mathematical coordinates (y-axis pointing UP) and
|
211
|
-
// scaled to the constraint editor chart area, cf. global constants
|
283
|
+
// scaled to the constraint editor chart area, cf. global constants.
|
212
284
|
// defined for the constraint editor.
|
213
285
|
return (this.oX + x * this.scale) + ',' + (this.oY - y * this.scale);
|
214
286
|
}
|
@@ -396,10 +468,52 @@ class ConstraintEditor {
|
|
396
468
|
}
|
397
469
|
this.equation_div.innerHTML = segeq;
|
398
470
|
}
|
471
|
+
|
472
|
+
positionPoint() {
|
473
|
+
// Prompt modeler for precise point coordinates.
|
474
|
+
if(this.selected_point < 0) return;
|
475
|
+
const
|
476
|
+
md = this.point_modal,
|
477
|
+
pc = this.point_div.innerHTML.split(', ');
|
478
|
+
md.element('x').value = pc[0].substring(1);
|
479
|
+
md.element('y').value = pc[1].substring(0, pc[1].length - 1);
|
480
|
+
md.show('x');
|
481
|
+
}
|
482
|
+
|
483
|
+
validPointCoordinate(c) {
|
484
|
+
// Return floating point for coordinate `c` (= 'x' or 'y') or FALSE
|
485
|
+
// if input is invalid.
|
486
|
+
const
|
487
|
+
md = this.point_modal,
|
488
|
+
e = md.element(c),
|
489
|
+
v = safeStrToFloat(e.value, false);
|
490
|
+
if(v === false || v < 0 || v > 100) {
|
491
|
+
UI.warn('Invalid boundline point coordinate');
|
492
|
+
e.select();
|
493
|
+
e.focus();
|
494
|
+
return false;
|
495
|
+
}
|
496
|
+
return v;
|
497
|
+
}
|
498
|
+
|
499
|
+
setPointPosition() {
|
500
|
+
// Check coordinates, and if valid update those of the selected point.
|
501
|
+
// Otherwise, warn user and select offending input field.
|
502
|
+
if(this.selected_point < 0) return;
|
503
|
+
const
|
504
|
+
x = this.validPointCoordinate('x'),
|
505
|
+
y = this.validPointCoordinate('y');
|
506
|
+
if(x !== false && y !== false) {
|
507
|
+
this.dragged_point = this.selected_point;
|
508
|
+
this.movePoint(x, y);
|
509
|
+
this.dragged_point = -1;
|
510
|
+
this.point_modal.hide();
|
511
|
+
}
|
512
|
+
}
|
399
513
|
|
400
|
-
movePoint() {
|
401
|
-
//
|
402
|
-
// Use l as shorthand for the selected line
|
514
|
+
movePoint(x, y) {
|
515
|
+
// Move the dragged point of the selected bound line.
|
516
|
+
// Use `l` as shorthand for the selected line.
|
403
517
|
const
|
404
518
|
l = this.selected,
|
405
519
|
pi = this.dragged_point,
|
@@ -411,8 +525,8 @@ class ConstraintEditor {
|
|
411
525
|
py = p[1],
|
412
526
|
minx = (pi === 0 ? 0 : (pi === lpi ? 100 : l.points[pi - 1][0])),
|
413
527
|
maxx = (pi === 0 ? 0 : (pi === lpi ? 100 : l.points[pi + 1][0])),
|
414
|
-
newx = Math.min(maxx, Math.max(minx,
|
415
|
-
newy = Math.min(100, Math.max(0,
|
528
|
+
newx = Math.min(maxx, Math.max(minx, x)),
|
529
|
+
newy = Math.min(100, Math.max(0, y));
|
416
530
|
// No action needed unless point has been moved
|
417
531
|
if(newx !== px || newy !== py) {
|
418
532
|
p[0] = newx;
|
@@ -424,17 +538,22 @@ class ConstraintEditor {
|
|
424
538
|
|
425
539
|
updateStatus() {
|
426
540
|
// Displays cursor position as X and Y (in chart coordinates), and updates
|
427
|
-
// controls
|
541
|
+
// controls.
|
428
542
|
this.pos_x_div.innerHTML = 'X = ' + this.pos_x.toPrecision(3);
|
429
543
|
this.pos_y_div.innerHTML = 'Y = ' + this.pos_y.toPrecision(3);
|
544
|
+
this.point_div.innerHTML = '';
|
430
545
|
const blbtns = 'add-point del-bl';
|
431
546
|
if(this.selected) {
|
432
547
|
if(this.selected_point >= 0) {
|
433
548
|
const p = this.selected.points[this.selected_point];
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
549
|
+
let px = p[0].toPrecision(3),
|
550
|
+
py = p[1].toPrecision(3),
|
551
|
+
cp = this.customPoint(p[0], p[1]);
|
552
|
+
if(cp & 1) px = p[0].toPrecision(10).replace(/[0]+$/, '')
|
553
|
+
.replace(/\.$/, '');
|
554
|
+
if(cp & 2) py = p[1].toPrecision(10).replace(/[0]+$/, '')
|
555
|
+
.replace(/\.$/, '');
|
556
|
+
this.point_div.innerHTML = `(${px}, ${py})`;
|
438
557
|
}
|
439
558
|
// Check whether selected point is an end point
|
440
559
|
const ep = this.selected_point === 0 ||
|
@@ -591,13 +710,19 @@ class ConstraintEditor {
|
|
591
710
|
width = 1.5;
|
592
711
|
color = this.line_color[l.type];
|
593
712
|
}
|
594
|
-
const
|
713
|
+
const
|
714
|
+
cfs = `fill="${color}" stroke="${color}" stroke-width="${width}"`,
|
715
|
+
icfs = 'fill="white" stroke="white" stroke-width="1"';
|
595
716
|
for(let i = 0; i < l.points.length; i++) {
|
596
717
|
const
|
597
718
|
px = l.points[i][0],
|
598
719
|
py = l.points[i][1];
|
599
720
|
pp.push(this.point(px, py));
|
600
721
|
dots += `<circle ${this.circleCenter(px, py)} r="3" ${cfs}></circle>`;
|
722
|
+
// Draw "custom points" with a white inner circle.
|
723
|
+
if(this.customPoint(px, py)) {
|
724
|
+
dots += `<circle ${this.circleCenter(px, py)} r="1.5" ${icfs}></circle>`;
|
725
|
+
}
|
601
726
|
}
|
602
727
|
const cp = 'M' + pp.join('L');
|
603
728
|
// For EQ bound lines, the line path is the contour; this will be
|
@@ -549,9 +549,9 @@ class GUIController extends Controller {
|
|
549
549
|
this.buttons.actors.addEventListener('click',
|
550
550
|
() => ACTOR_MANAGER.showDialog());
|
551
551
|
this.buttons.diagram.addEventListener('click',
|
552
|
-
() => FILE_MANAGER.renderDiagramAsPNG());
|
552
|
+
() => FILE_MANAGER.renderDiagramAsPNG(event.shiftKey));
|
553
553
|
this.buttons.savediagram.addEventListener('click',
|
554
|
-
() => FILE_MANAGER.saveDiagramAsSVG());
|
554
|
+
() => FILE_MANAGER.saveDiagramAsSVG(event.shiftKey));
|
555
555
|
this.buttons.receiver.addEventListener('click',
|
556
556
|
() => RECEIVER.toggle());
|
557
557
|
// NOTE: All draggable & resizable dialogs "toggle" show/hide.
|
@@ -2154,7 +2154,7 @@ class GUIController extends Controller {
|
|
2154
2154
|
if(topmod && topmod.id === 'constraint-modal') {
|
2155
2155
|
if([37, 38, 39, 40].indexOf(e.keyCode) >= 0) {
|
2156
2156
|
e.preventDefault();
|
2157
|
-
CONSTRAINT_EDITOR.arrowKey(e
|
2157
|
+
CONSTRAINT_EDITOR.arrowKey(e);
|
2158
2158
|
return;
|
2159
2159
|
}
|
2160
2160
|
}
|
@@ -342,11 +342,18 @@ class GUIFileManager {
|
|
342
342
|
});
|
343
343
|
}
|
344
344
|
|
345
|
-
renderDiagramAsPNG() {
|
345
|
+
renderDiagramAsPNG(tight) {
|
346
|
+
// When `tight` is TRUE, add no whitespace around the diagram.
|
346
347
|
window.localStorage.removeItem('png-url');
|
347
|
-
|
348
|
-
|
349
|
-
|
348
|
+
if(tight) {
|
349
|
+
// First align to grid and then fit to size.
|
350
|
+
MODEL.alignToGrid();
|
351
|
+
UI.paper.fitToSize(1);
|
352
|
+
} else {
|
353
|
+
UI.paper.fitToSize();
|
354
|
+
MODEL.alignToGrid();
|
355
|
+
}
|
356
|
+
this.renderSVGAsPNG(UI.paper.opaqueSVG);
|
350
357
|
}
|
351
358
|
|
352
359
|
renderSVGAsPNG(svg) {
|
@@ -374,10 +381,17 @@ class GUIFileManager {
|
|
374
381
|
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
375
382
|
}
|
376
383
|
|
377
|
-
saveDiagramAsSVG() {
|
378
|
-
|
379
|
-
|
380
|
-
|
384
|
+
saveDiagramAsSVG(tight) {
|
385
|
+
// Output SVG as string with nodes and arrows 100% opaque.
|
386
|
+
if(tight) {
|
387
|
+
// First align to grid and then fit to size.
|
388
|
+
MODEL.alignToGrid();
|
389
|
+
UI.paper.fitToSize(1);
|
390
|
+
} else {
|
391
|
+
UI.paper.fitToSize();
|
392
|
+
MODEL.alignToGrid();
|
393
|
+
}
|
394
|
+
this.pushOutSVG(UI.paper.opaqueSVG);
|
381
395
|
}
|
382
396
|
|
383
397
|
pushOutSVG(svg) {
|
@@ -132,7 +132,11 @@ class Shape {
|
|
132
132
|
const ts = UI.paper.newSVGElement('tspan');
|
133
133
|
ts.setAttribute('x', x);
|
134
134
|
ts.setAttribute('dy', fh);
|
135
|
-
|
135
|
+
// NOTE: Non-breaking space must now (inside a TSPAN) be converted
|
136
|
+
// to normal spaces, or they will be rendered as ' ' and this
|
137
|
+
// will cause the SVG to break when it is inserted as picture into
|
138
|
+
// an MS Word document.
|
139
|
+
ts.textContent = lines[i].replaceAll('\u00A0', ' ');
|
136
140
|
el.appendChild(ts);
|
137
141
|
}
|
138
142
|
this.element.appendChild(el);
|
@@ -348,6 +352,13 @@ class Paper {
|
|
348
352
|
this.clear();
|
349
353
|
}
|
350
354
|
|
355
|
+
get opaqueSVG() {
|
356
|
+
// Return SVG as string with nodes and arrows 100% opaque.
|
357
|
+
// NOTE: The semi-transparent ovals behind rates on links have
|
358
|
+
// opacity 0.8 and hence are not affected.
|
359
|
+
return this.svg.outerHTML.replaceAll(' opacity="0.9"', ' opacity="1"');
|
360
|
+
}
|
361
|
+
|
351
362
|
clear() {
|
352
363
|
// First, clear the entire SVG
|
353
364
|
this.clearSVGElement(this.svg);
|
@@ -471,12 +482,12 @@ class Paper {
|
|
471
482
|
}
|
472
483
|
|
473
484
|
clearSVGElement(el) {
|
474
|
-
//
|
485
|
+
// Clear all sub-nodes of the specified SVG node.
|
475
486
|
if(el) while(el.lastChild) el.removeChild(el.lastChild);
|
476
487
|
}
|
477
488
|
|
478
489
|
addSVGAttributes(el, obj) {
|
479
|
-
//
|
490
|
+
// Add attributes specified by `obj` to (SVG) element `el`.
|
480
491
|
for(let prop in obj) {
|
481
492
|
if(obj.hasOwnProperty(prop)) el.setAttribute(prop, obj[prop]);
|
482
493
|
}
|
@@ -686,35 +697,35 @@ class Paper {
|
|
686
697
|
return el;
|
687
698
|
}
|
688
699
|
|
689
|
-
fitToSize() {
|
700
|
+
fitToSize(margin=30) {
|
690
701
|
// Adjust the dimensions of the main SVG to fit the graph plus 15px margin
|
691
702
|
// all around
|
692
703
|
this.removeInvisibleSVG();
|
693
704
|
const
|
694
705
|
bb = this.svg.getBBox(),
|
695
|
-
w = bb.width +
|
696
|
-
h = bb.height +
|
706
|
+
w = bb.width + margin,
|
707
|
+
h = bb.height + margin;
|
697
708
|
if(w !== this.width || h !== this.height) {
|
698
|
-
MODEL.translateGraph(-bb.x +
|
709
|
+
MODEL.translateGraph(-bb.x + margin / 2, -bb.y + margin);
|
699
710
|
this.width = w;
|
700
711
|
this.height = h;
|
701
712
|
this.svg.setAttribute('width', this.width);
|
702
713
|
this.svg.setAttribute('height', this.height);
|
703
714
|
this.zoom_factor = 1;
|
704
715
|
this.zoom_label.innerHTML = Math.round(100 / this.zoom_factor) + '%';
|
705
|
-
this.extend();
|
716
|
+
this.extend(margin);
|
706
717
|
}
|
707
718
|
}
|
708
719
|
|
709
|
-
extend() {
|
720
|
+
extend(margin=30) {
|
710
721
|
// Adjust the paper size to fit all objects WITHOUT changing the origin (0, 0)
|
711
722
|
// NOTE: keep a minimum page size to keep the scrolling more "natural"
|
712
723
|
this.removeInvisibleSVG();
|
713
724
|
const
|
714
725
|
bb = this.svg.getBBox(),
|
715
726
|
// Let `w` and `h` be the actual width and height in pixels
|
716
|
-
w = bb.x + bb.width +
|
717
|
-
h = bb.y + bb.height +
|
727
|
+
w = bb.x + bb.width + margin,
|
728
|
+
h = bb.y + bb.height + margin,
|
718
729
|
// Let `ccw` and `cch` be the size of the scrollable area
|
719
730
|
ccw = w / this.zoom_factor,
|
720
731
|
cch = h / this.zoom_factor;
|
@@ -1391,7 +1402,7 @@ class Paper {
|
|
1391
1402
|
// Add 2px margin
|
1392
1403
|
shift = 2;
|
1393
1404
|
const lfd = (luc.actualDelay(MODEL.t));
|
1394
|
-
if(lfd
|
1405
|
+
if(lfd != 0) {
|
1395
1406
|
// If delay, draw it in a circle behind arrow head
|
1396
1407
|
s = lfd;
|
1397
1408
|
bb = this.numberSize(s, 7);
|
@@ -1419,6 +1430,7 @@ class Paper {
|
|
1419
1430
|
{stroke:stroke_color, 'stroke-width': 0.5, fill: 'white'});
|
1420
1431
|
// MU symbol does not center prettily => raise by 1 px
|
1421
1432
|
const raise = (luc.multiplier === VM.LM_MEAN ||
|
1433
|
+
luc.multiplier === VM.LM_STARTUP ||
|
1422
1434
|
luc.multiplier === VM.LM_THROUGHPUT ? 1 :
|
1423
1435
|
(luc.multiplier === VM.LM_PEAK_INC ? 1.5 : 0));
|
1424
1436
|
arrw.shape.addText(epx, epy - raise, VM.LM_SYMBOLS[luc.multiplier],
|
@@ -1462,12 +1474,13 @@ class Paper {
|
|
1462
1474
|
epy = arrw.from_y + (shift + bi) * dy / l;
|
1463
1475
|
font_color = this.palette.produced;
|
1464
1476
|
}
|
1465
|
-
// Draw the rate in a semi-transparent white
|
1466
|
-
arrw.shape.
|
1477
|
+
// Draw the rate in a semi-transparent white roundbox.
|
1478
|
+
arrw.shape.addRect(epx, epy, tw, th,
|
1479
|
+
{fill: 'white', opacity: 0.8, rx: 2, ry: 2});
|
1467
1480
|
arrw.shape.addNumber(epx, epy, s, {fill: font_color, 'font-style': rrfs});
|
1468
1481
|
|
1469
1482
|
// Draw the share of cost (only if relevant and > 0) behind the rate
|
1470
|
-
// in a pale yellow filled box
|
1483
|
+
// in a pale yellow filled box.
|
1471
1484
|
if(MODEL.infer_cost_prices && luc.share_of_cost > 0) {
|
1472
1485
|
// Keep the right distance from the rate: the midpoint should
|
1473
1486
|
// increase by a varying length: number lengths / 2 when arrow is
|
@@ -8437,8 +8437,8 @@ class Link {
|
|
8437
8437
|
// Scales delay expression value to number of time steps on model
|
8438
8438
|
// time scale
|
8439
8439
|
let d = Math.floor(VM.SIG_DIF_FROM_ZERO + this.flow_delay.result(t));
|
8440
|
-
// NOTE:
|
8441
|
-
|
8440
|
+
// NOTE: Negative values are permitted. This might invalidate cost
|
8441
|
+
// price calculation -- to be checked!!
|
8442
8442
|
return d;
|
8443
8443
|
}
|
8444
8444
|
|
@@ -6954,7 +6954,7 @@ function VMI_log(x) {
|
|
6954
6954
|
if(d !== false) {
|
6955
6955
|
if(DEBUGGING) console.log('LOG (' + d.join(', ') + ')');
|
6956
6956
|
try {
|
6957
|
-
d = Math.
|
6957
|
+
d = Math.log(d[1]) / Math.log(d[0]);
|
6958
6958
|
} catch(err) {
|
6959
6959
|
d = VM.BAD_CALC;
|
6960
6960
|
}
|
@@ -8093,9 +8093,9 @@ const
|
|
8093
8093
|
'%', '^', 'log', '|'],
|
8094
8094
|
DYADIC_CODES = [
|
8095
8095
|
VMI_concat, VMI_if_then, VMI_if_else, VMI_or, VMI_and,
|
8096
|
-
VMI_eq, VMI_ne, VMI_ne,
|
8097
|
-
|
8098
|
-
|
8096
|
+
VMI_eq, VMI_ne, VMI_ne, VMI_gt, VMI_lt, VMI_ge, VMI_le,
|
8097
|
+
VMI_add, VMI_sub, VMI_mul, VMI_div, VMI_mod,
|
8098
|
+
VMI_power, VMI_log, VMI_replace_undefined],
|
8099
8099
|
|
8100
8100
|
// Compiler checks for random codes as they make an expression dynamic
|
8101
8101
|
RANDOM_CODES = [VMI_binomial, VMI_exponential, VMI_normal, VMI_poisson,
|