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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.6.3",
3
+ "version": "1.6.5",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
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">
@@ -1922,8 +1922,8 @@ div.menu-item:hover {
1922
1922
  #constraint-point {
1923
1923
  position: absolute;
1924
1924
  top: 393px;
1925
- left: 160px;
1926
- width: 75px;
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', '#00ffff', '#a800ff'];
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(k) {
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/3);
193
- } else if (k === 38 && p[1] <= 299/3) {
194
- p[1] += 1/3;
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/3);
197
- } else if (k === 40 && p[1] >= 1/3) {
198
- p[1] -= 1/3;
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
- // Returns a string denoting the point (x, y) in SVG notation, assuming
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
- // Moves the dragged point of the selected bound line
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, this.pos_x)),
415
- newy = Math.min(100, Math.max(0, this.pos_y));
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
- this.point_div.innerHTML =
435
- `(${p[0].toPrecision(3)}, ${p[1].toPrecision(3)})`;
436
- } else {
437
- this.point_div.innerHTML = '';
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 cfs = `fill="${color}" stroke="${color}" stroke-width="${width}"`;
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.keyCode);
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
- UI.paper.fitToSize();
348
- MODEL.alignToGrid();
349
- this.renderSVGAsPNG(UI.paper.svg.outerHTML);
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
- UI.paper.fitToSize();
379
- MODEL.alignToGrid();
380
- this.pushOutSVG(UI.paper.outerHTML);
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
- ts.textContent = lines[i];
135
+ // NOTE: Non-breaking space must now (inside a TSPAN) be converted
136
+ // to normal spaces, or they will be rendered as '&nbsp;' 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
- // Clears all sub-nodes of the specified SVG node
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
- // Adds attributes specified by `obj` to (SVG) element `el`
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 + 30,
696
- h = bb.height + 30;
706
+ w = bb.width + margin,
707
+ h = bb.height + margin;
697
708
  if(w !== this.width || h !== this.height) {
698
- MODEL.translateGraph(-bb.x + 15, -bb.y + 25);
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 + 30,
717
- h = bb.y + bb.height + 30,
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 > 0) {
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 ellipse
1466
- arrw.shape.addEllipse(epx, epy, tw/2, th/2, {fill: 'white', opacity: 0.8});
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: negative values are interpreted as 0 (no warning)
8441
- if(d <= 0) return 0;
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.exp(Math.log(d[1]) / Math.log(d[0]));
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
- VMI_gt, VMI_lt, VMI_ge, VMI_le, VMI_add, VMI_sub, VMI_mul, VMI_div,
8098
- VMI_mod, VMI_power, VMI_log, VMI_replace_undefined],
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,