linny-r 2.0.8 → 2.0.10
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.md +3 -40
- package/package.json +6 -2
- package/server.js +19 -157
- package/static/images/solve-not-same-changed.png +0 -0
- package/static/images/solve-not-same-not-changed.png +0 -0
- package/static/images/solve-same-changed.png +0 -0
- package/static/images/solve-same-not-changed.png +0 -0
- package/static/index.html +137 -20
- package/static/linny-r.css +260 -23
- package/static/scripts/iro.min.js +7 -7
- package/static/scripts/linny-r-ctrl.js +126 -85
- package/static/scripts/linny-r-gui-actor-manager.js +23 -33
- package/static/scripts/linny-r-gui-chart-manager.js +56 -53
- package/static/scripts/linny-r-gui-constraint-editor.js +10 -14
- package/static/scripts/linny-r-gui-controller.js +644 -260
- package/static/scripts/linny-r-gui-dataset-manager.js +64 -66
- package/static/scripts/linny-r-gui-documentation-manager.js +11 -17
- package/static/scripts/linny-r-gui-equation-manager.js +22 -22
- package/static/scripts/linny-r-gui-experiment-manager.js +124 -141
- package/static/scripts/linny-r-gui-expression-editor.js +26 -12
- package/static/scripts/linny-r-gui-file-manager.js +42 -48
- package/static/scripts/linny-r-gui-finder.js +294 -55
- package/static/scripts/linny-r-gui-model-autosaver.js +2 -4
- package/static/scripts/linny-r-gui-monitor.js +35 -41
- package/static/scripts/linny-r-gui-paper.js +42 -70
- package/static/scripts/linny-r-gui-power-grid-manager.js +31 -34
- package/static/scripts/linny-r-gui-receiver.js +1 -2
- package/static/scripts/linny-r-gui-repository-browser.js +44 -46
- package/static/scripts/linny-r-gui-scale-unit-manager.js +32 -32
- package/static/scripts/linny-r-gui-sensitivity-analysis.js +61 -67
- package/static/scripts/linny-r-gui-undo-redo.js +94 -95
- package/static/scripts/linny-r-milp.js +20 -24
- package/static/scripts/linny-r-model.js +1932 -2274
- package/static/scripts/linny-r-utils.js +27 -27
- package/static/scripts/linny-r-vm.js +900 -998
- package/static/show-png.html +0 -113
@@ -192,58 +192,57 @@ class Controller {
|
|
192
192
|
// NOTE: Add '' in case string is a number
|
193
193
|
const lines = ('' + string).split('\n');
|
194
194
|
let w = 0;
|
195
|
-
for(
|
196
|
-
w = Math.max(w,
|
195
|
+
for(const l of lines) {
|
196
|
+
w = Math.max(w, l.length * cw);
|
197
197
|
}
|
198
198
|
return {width: w, height: lines.length * ch};
|
199
199
|
}
|
200
200
|
|
201
201
|
stringToLineArray(string, width=100, fsize=8) {
|
202
|
-
//
|
203
|
-
// while preserving newlines -- used to format text of notes
|
202
|
+
// Return an array of strings wrapped to given width at given font size
|
203
|
+
// while preserving newlines -- used to format text of notes.
|
204
204
|
const
|
205
205
|
multi = [],
|
206
206
|
lines = string.split('\n'),
|
207
|
-
ll = lines.length,
|
208
207
|
// If no paper, assume 144 px/inch, so 1 pt = 2 px
|
209
208
|
fh = (this.paper ? this.paper.font_heights[fsize] : 2 * fsize),
|
210
209
|
scalar = fh / 2;
|
211
|
-
for(
|
210
|
+
for(const l of lines) {
|
212
211
|
// NOTE: interpret two spaces as a "non-breaking" space
|
213
|
-
const words =
|
212
|
+
const words = l.replace(/ /g, '\u00A0').trim().split(/ +/);
|
214
213
|
// Split words at '-' when wider than width
|
215
|
-
for(let
|
216
|
-
if(words[
|
217
|
-
const sw = words[
|
218
|
-
if(sw.length > 1) {
|
219
|
-
// Replace
|
220
|
-
words[
|
214
|
+
for(let i = 0; i < words.length; i++) {
|
215
|
+
if(words[i].length * scalar > width) {
|
216
|
+
const sw = words[i].split('-');
|
217
|
+
if(sw[0] && sw.length > 1) {
|
218
|
+
// Replace i-th word by last fragment of split string
|
219
|
+
words[i] = sw.pop();
|
221
220
|
// Insert remaining fragments before
|
222
|
-
while(sw.length > 0) words.splice(
|
221
|
+
while(sw.length > 0) words.splice(i, 0, sw.pop() + '-');
|
223
222
|
}
|
224
223
|
}
|
225
224
|
}
|
226
|
-
let line = words
|
227
|
-
for(
|
225
|
+
let line = words.shift() + ' ';
|
226
|
+
for(const word of words) {
|
228
227
|
const
|
229
|
-
l = line +
|
228
|
+
l = line + word + ' ',
|
230
229
|
w = (l.length - 1) * scalar;
|
231
|
-
if (w > width
|
230
|
+
if (w > width) {
|
232
231
|
const
|
233
232
|
nl = line.trim(),
|
234
233
|
nw = Math.floor(nl.length * scalar);
|
235
234
|
multi.push(nl);
|
236
235
|
// If width of added line exceeds the given width, adjust width
|
237
|
-
// so that following lines fill out better
|
236
|
+
// so that following lines fill out better.
|
238
237
|
width = Math.max(width, nw);
|
239
|
-
line =
|
238
|
+
line = word + ' ';
|
240
239
|
} else {
|
241
240
|
line = l;
|
242
241
|
}
|
243
242
|
}
|
244
243
|
line = line.trim();
|
245
244
|
// NOTE: Chrome and Safari ignore empty lines in SVG text; as a workaround,
|
246
|
-
// we add a non-breaking space to lines containing only whitespace
|
245
|
+
// we add a non-breaking space to lines containing only whitespace.
|
247
246
|
if(!line) line = '\u00A0';
|
248
247
|
multi.push(line);
|
249
248
|
}
|
@@ -280,19 +279,19 @@ class Controller {
|
|
280
279
|
// Methods to ensure proper naming of entities.
|
281
280
|
|
282
281
|
cleanName(name) {
|
283
|
-
//
|
282
|
+
// Return `name` without the object-attribute separator |, backslashes,
|
284
283
|
// and leading and trailing whitespace, and with all internal whitespace
|
285
284
|
// reduced to a single space.
|
286
285
|
name = name.replace(this.OA_SEPARATOR, ' ')
|
287
286
|
.replace(/\||\\/g, ' ').trim()
|
288
287
|
.replace(/\s\s+/g, ' ');
|
289
|
-
// NOTE:
|
288
|
+
// NOTE: This may still result in a single space, which is not a name.
|
290
289
|
if(name === ' ') return '';
|
291
290
|
return name;
|
292
291
|
}
|
293
292
|
|
294
293
|
validName(name) {
|
295
|
-
//
|
294
|
+
// Return TRUE if `name` is a valid Linny-R entity name. These names
|
296
295
|
// must not be empty strings, may not contain brackets, backslashes or
|
297
296
|
// vertical bars, may not end with a colon, and must start with an
|
298
297
|
// underscore, a letter or a digit.
|
@@ -335,7 +334,7 @@ class Controller {
|
|
335
334
|
}
|
336
335
|
|
337
336
|
completePrefix(name) {
|
338
|
-
//
|
337
|
+
// Return the prefix part (including the final colon plus space),
|
339
338
|
// or the empty string if none.
|
340
339
|
const p = UI.prefixesAndName(name);
|
341
340
|
p[p.length - 1] = '';
|
@@ -343,6 +342,7 @@ class Controller {
|
|
343
342
|
}
|
344
343
|
|
345
344
|
sharedPrefix(n1, n2) {
|
345
|
+
// Return the longest series of prefixes that `n1` and `n2` have in common.
|
346
346
|
const
|
347
347
|
pan1 = this.prefixesAndName(n1),
|
348
348
|
pan2 = this.prefixesAndName(n2),
|
@@ -350,7 +350,7 @@ class Controller {
|
|
350
350
|
shared = [];
|
351
351
|
let i = 0;
|
352
352
|
while(i < l && ciCompare(pan1[i], pan2[i]) === 0) {
|
353
|
-
// NOTE:
|
353
|
+
// NOTE: If identical except for case, prefer "Abc" over "aBc".
|
354
354
|
shared.push(pan1[i] < pan2[i] ? pan1[i] : pan2[i]);
|
355
355
|
i++;
|
356
356
|
}
|
@@ -358,26 +358,90 @@ class Controller {
|
|
358
358
|
}
|
359
359
|
|
360
360
|
colonPrefixedName(name, prefix) {
|
361
|
-
//
|
361
|
+
// Replace a leading colon in `name` by `prefix`.
|
362
362
|
// If `name` identifies a link or a constraint, this is applied to
|
363
363
|
// both node names.
|
364
|
+
// NOTE: To give the modeler more control over what to use as prefix,
|
365
|
+
// successive colons indicate that a shorter prefix should be used.
|
366
|
+
// For example, when the prefix is XXX: YYY: ZZZ, two colons mean
|
367
|
+
// "use only XXX: YYY:", three colons "use only XXX:", etc.
|
364
368
|
const
|
365
369
|
arrow = (name.indexOf(this.LINK_ARROW) >= 0 ?
|
366
370
|
this.LINK_ARROW : this.CONSTRAINT_ARROW),
|
367
371
|
nodes = name.split(arrow);
|
368
372
|
for(let i = 0; i < nodes.length; i++) {
|
369
|
-
|
373
|
+
// First check for the special case of just a leading colon plus
|
374
|
+
// possibly an entity attribute.
|
375
|
+
// NOTE:
|
376
|
+
const m = nodes[i].match(/^(:+)\s*(\|[^\|]*)/);
|
377
|
+
if(m) {
|
378
|
+
// Split prefix in parts.
|
379
|
+
const p = prefix.split(UI.PREFIXER);
|
380
|
+
// Remove last (empty!) substring.
|
381
|
+
p.pop();
|
382
|
+
// Shorten prefix by the number of successive colons minus 1.
|
383
|
+
for(let i = 1; i < m[1].length; i++) p.pop();
|
384
|
+
p.push('');
|
385
|
+
// New name is just the (shortened) prefix without its last ": ".
|
386
|
+
nodes[i] = p.join(UI.PREFIXER).replace(/:\s*$/, '') + m[2];
|
387
|
+
} else {
|
388
|
+
// Prefix is typically leading, so try to replace this first.
|
389
|
+
const m = nodes[i].match(/^(:+)/);
|
390
|
+
if(m) {
|
391
|
+
// Shorten prefix by the number of successive colons minus 1.
|
392
|
+
let p = prefix.split(UI.PREFIXER);
|
393
|
+
p.pop();
|
394
|
+
for(let i = 1; i < m[1].length; i++) p.pop();
|
395
|
+
p.push('');
|
396
|
+
nodes[i] = nodes[i].replace(/^:+\s*/, p.join(UI.PREFIXER));
|
397
|
+
} else {
|
398
|
+
// If no change, try to replace an embedded double prefix.
|
370
399
|
// NOTE: An embedded double prefix, e.g., "xxx: : yyy" indicates
|
371
400
|
// that the second colon+space should be replaced by the prefix.
|
372
|
-
|
373
|
-
|
374
|
-
|
401
|
+
const m = nodes[i].match(/(\w+):\s+(:+)\s+(\w+)/);
|
402
|
+
if(m) {
|
403
|
+
// Shorten prefix by the number of successive colons minus 1.
|
404
|
+
let p = prefix.split(UI.PREFIXER);
|
405
|
+
p.pop();
|
406
|
+
for(let i = 1; i < m[2].length; i++) p.pop();
|
407
|
+
p.push('');
|
408
|
+
prefix = p.join(UI.PREFIXER);
|
409
|
+
nodes[i] = `${m[1]}: ${prefix}${m[3]}`;
|
410
|
+
}
|
411
|
+
}
|
412
|
+
}
|
375
413
|
}
|
376
414
|
return nodes.join(arrow);
|
377
415
|
}
|
416
|
+
|
417
|
+
entityPrefix(name) {
|
418
|
+
// Return the prefix of `name` with its trailing colon+space.
|
419
|
+
const
|
420
|
+
arrow = (name.indexOf(this.LINK_ARROW) >= 0 ?
|
421
|
+
this.LINK_ARROW : this.CONSTRAINT_ARROW),
|
422
|
+
nodes = name.split(arrow);
|
423
|
+
if(nodes.length === 1) return this.completePrefix(name);
|
424
|
+
// For names of links and constraints, it depends:
|
425
|
+
const
|
426
|
+
fn = nodes[0],
|
427
|
+
tn = nodes[1];
|
428
|
+
if(fn.indexOf(UI.PREFIXER) >= 0) {
|
429
|
+
if(tn.indexOf(UI.PREFIXER) >= 0) {
|
430
|
+
// If BOTH nodes are prefixed, use the longest prefix that these
|
431
|
+
// nodes have in common...
|
432
|
+
return UI.sharedPrefix(fn, tn) + UI.PREFIXER;
|
433
|
+
}
|
434
|
+
// .. otherwise, return the FROM node prefix.
|
435
|
+
return UI.completePrefix(fn);
|
436
|
+
}
|
437
|
+
// No FROM node prefix => return the TO node prefix (if any)
|
438
|
+
if(tn.indexOf(UI.PREFIXER) >= 0) return UI.completePrefix(tn);
|
439
|
+
// No prefixers => empty prefix.
|
440
|
+
return '';
|
441
|
+
}
|
378
442
|
|
379
443
|
tailNumber(name) {
|
380
|
-
//
|
444
|
+
// Return the string of digits at the end of `name`. If not there,
|
381
445
|
// check prefixes (if any) *from right to left* for a tail number.
|
382
446
|
// Thus, the number that is "closest" to the name part is returned.
|
383
447
|
const pan = UI.prefixesAndName(name);
|
@@ -429,7 +493,6 @@ class Controller {
|
|
429
493
|
return pan1.length - pan2.length;
|
430
494
|
}
|
431
495
|
|
432
|
-
|
433
496
|
nameToID(name) {
|
434
497
|
// Return a name in lower case with link arrow replaced by three
|
435
498
|
// underscores, constraint link arrow by four underscores, and spaces
|
@@ -444,10 +507,11 @@ class Controller {
|
|
444
507
|
// Empty string signals failure.
|
445
508
|
return '';
|
446
509
|
}
|
447
|
-
// NOTE: Replace single quotes by Unicode apostrophe
|
448
|
-
//
|
510
|
+
// NOTE: Replace single quotes by Unicode apostrophe, and double quotes
|
511
|
+
// by Unicode mono-space " so that quotes cannot interfere with string
|
512
|
+
// delimiters used in JavaScript or HTML.
|
449
513
|
return name.toLowerCase().replace(/\s/g, '_')
|
450
|
-
.
|
514
|
+
.replaceAll("'", '\u2019').replaceAll('"', '\uff02');
|
451
515
|
}
|
452
516
|
|
453
517
|
htmlEquationName(n) {
|
@@ -654,9 +718,8 @@ class RepositoryBrowser {
|
|
654
718
|
.then((data) => {
|
655
719
|
if(UI.postResponseOK(data)) {
|
656
720
|
// NOTE: Trim to prevent empty name strings.
|
657
|
-
const
|
658
|
-
|
659
|
-
this.addRepository(rl[i].trim());
|
721
|
+
for(const r of data.trim().split('\n')) {
|
722
|
+
this.addRepository(r.trim());
|
660
723
|
}
|
661
724
|
}
|
662
725
|
// NOTE: Set index to first repository on list (typically the
|
@@ -669,23 +732,12 @@ class RepositoryBrowser {
|
|
669
732
|
|
670
733
|
repositoryByName(n) {
|
671
734
|
// Return the repository having name `n` if already known, or NULL.
|
672
|
-
for(
|
673
|
-
if(
|
674
|
-
return this.repositories[i];
|
675
|
-
}
|
735
|
+
for(const r of this.repositories) {
|
736
|
+
if(r.name === n) return r;
|
676
737
|
}
|
677
738
|
return null;
|
678
739
|
}
|
679
740
|
|
680
|
-
asFileName(s) {
|
681
|
-
// Return string `s` with whitespace converted to a single dash, and
|
682
|
-
// special characters converted to underscores.
|
683
|
-
return s.normalize('NFKD').trim()
|
684
|
-
.replace(/[\s\-]+/g, '-')
|
685
|
-
.replace(/[^A-Za-z0-9_\-]/g, '_')
|
686
|
-
.replace(/^[\-\_]+|[\-\_]+$/g, '');
|
687
|
-
}
|
688
|
-
|
689
741
|
loadModuleAsModel() {
|
690
742
|
// Load selected module as model.
|
691
743
|
if(this.repository_index >= 0 && this.module_index >= 0) {
|
@@ -842,9 +894,7 @@ class ChartManager {
|
|
842
894
|
|
843
895
|
resetChartVectors() {
|
844
896
|
// Reset vectors of all charts.
|
845
|
-
for(
|
846
|
-
MODEL.charts[i].resetVectors();
|
847
|
-
}
|
897
|
+
for(const c of MODEL.charts) c.resetVectors();
|
848
898
|
}
|
849
899
|
|
850
900
|
promptForWildcardIndices(chart, dsm) {
|
@@ -916,9 +966,8 @@ class SensitivityAnalysis {
|
|
916
966
|
// Clear results from previous analysis.
|
917
967
|
this.clearResults();
|
918
968
|
this.parameters = [];
|
919
|
-
for(
|
969
|
+
for(const p of MODEL.sensitivity_parameters) {
|
920
970
|
const
|
921
|
-
p = MODEL.sensitivity_parameters[i],
|
922
971
|
vn = p.split(UI.OA_SEPARATOR),
|
923
972
|
obj = MODEL.objectByName(vn[0]),
|
924
973
|
oax = (obj ? obj.attributeExpression(vn[1]) : null);
|
@@ -932,20 +981,16 @@ class SensitivityAnalysis {
|
|
932
981
|
}
|
933
982
|
}
|
934
983
|
this.chart = new Chart(this.chart_title);
|
935
|
-
for(
|
936
|
-
|
937
|
-
this.chart.addVariable(vn[0], vn[1]);
|
984
|
+
for(const o of MODEL.sensitivity_outcomes) {
|
985
|
+
this.chart.addVariable(...o.split(UI.OA_SEPARATOR));
|
938
986
|
}
|
939
987
|
this.experiment = new Experiment(this.experiment_title);
|
940
988
|
this.experiment.charts = [this.chart];
|
941
989
|
this.experiment.inferVariables();
|
942
990
|
// This experiment always uses the same combination: the base selectors.
|
943
991
|
const bs = MODEL.base_case_selectors.split(' ');
|
944
|
-
this.experiment.combinations = [];
|
945
992
|
// Add this combination N+1 times for N parameters.
|
946
|
-
|
947
|
-
this.experiment.combinations.push(bs);
|
948
|
-
}
|
993
|
+
this.experiment.combinations = Array(this.parameters.length + 1).fill(bs);
|
949
994
|
// NOTE: Model settings will not be changed, but will be restored after
|
950
995
|
// each run => store the original settings.
|
951
996
|
this.experiment.original_model_settings = MODEL.settingsString;
|
@@ -1045,17 +1090,15 @@ class SensitivityAnalysis {
|
|
1045
1090
|
this.perc = {};
|
1046
1091
|
this.shade = {};
|
1047
1092
|
this.data = {};
|
1048
|
-
const
|
1049
|
-
ol = MODEL.sensitivity_outcomes.length,
|
1050
|
-
rl = MODEL.sensitivity_runs.length;
|
1093
|
+
const ol = MODEL.sensitivity_outcomes.length;
|
1051
1094
|
if(ol === 0) return;
|
1052
1095
|
// Always find highest relative change.
|
1053
1096
|
let max_dif = 0;
|
1054
1097
|
for(let i = 0; i < ol; i++) {
|
1055
1098
|
this.data[i] = [];
|
1056
|
-
for(
|
1099
|
+
for(const run of MODEL.sensitivity_runs) {
|
1057
1100
|
// Get the selected statistic for each run to get an array of numbers.
|
1058
|
-
const rr =
|
1101
|
+
const rr = run.results[i];
|
1059
1102
|
if(!rr) {
|
1060
1103
|
this.data[i].push(VM.UNDEFINED);
|
1061
1104
|
} else if(sas === 'N') {
|
@@ -1162,8 +1205,7 @@ class ExperimentManager {
|
|
1162
1205
|
// Select charts having 1 or more variables, as only these are meaningful
|
1163
1206
|
// as the dependent variables of an experiment
|
1164
1207
|
this.suitable_charts.length = 0;
|
1165
|
-
for(
|
1166
|
-
const c = MODEL.charts[i];
|
1208
|
+
for(const c of MODEL.charts) {
|
1167
1209
|
if(c.variables.length > 0) this.suitable_charts.push(c);
|
1168
1210
|
}
|
1169
1211
|
}
|
@@ -1278,23 +1320,22 @@ class ExperimentManager {
|
|
1278
1320
|
}
|
1279
1321
|
xr.start();
|
1280
1322
|
this.showProgress(ci, p, n);
|
1281
|
-
// NOTE:
|
1323
|
+
// NOTE: First restore original model settings (setings may be partial!).
|
1282
1324
|
MODEL.parseSettings(x.original_model_settings);
|
1283
|
-
// Parse all active settings selector strings
|
1284
|
-
// NOTE:
|
1285
|
-
for(
|
1286
|
-
const ssel =
|
1325
|
+
// Parse all active settings selector strings.
|
1326
|
+
// NOTE: May be multiple strings; the later overwrite the earlier.
|
1327
|
+
for(const sel of x.settings_selectors) {
|
1328
|
+
const ssel = sel.split('|');
|
1287
1329
|
if(combi.indexOf(ssel[0]) >= 0) MODEL.parseSettings(ssel[1]);
|
1288
1330
|
}
|
1289
|
-
// Also set the correct round sequence
|
1290
|
-
// NOTE:
|
1291
|
-
for(
|
1292
|
-
const asel = x.actor_selectors[i];
|
1331
|
+
// Also set the correct round sequence.
|
1332
|
+
// NOTE: If no match, default is retained.
|
1333
|
+
for(const asel of x.actor_selectors) {
|
1293
1334
|
if(combi.indexOf(asel.selector) >= 0) {
|
1294
1335
|
MODEL.round_sequence = asel.round_sequence;
|
1295
1336
|
}
|
1296
1337
|
}
|
1297
|
-
// Only now compute the simulation run time (number of time steps)
|
1338
|
+
// Only now compute the simulation run time (number of time steps).
|
1298
1339
|
xr.time_steps = MODEL.end_period - MODEL.start_period + 1;
|
1299
1340
|
VM.callback = this.callback;
|
1300
1341
|
// NOTE: Asynchronous call. All follow-up actions must be performed
|
@@ -1304,18 +1345,18 @@ class ExperimentManager {
|
|
1304
1345
|
}
|
1305
1346
|
|
1306
1347
|
processRun() {
|
1307
|
-
// This method is called by the solveBlocks method of the Virtual Machine
|
1348
|
+
// This method is called by the solveBlocks method of the Virtual Machine.
|
1308
1349
|
const x = MODEL.running_experiment;
|
1309
1350
|
if(!x) return;
|
1310
1351
|
const aci = x.active_combination_index;
|
1311
1352
|
if(MODEL.solved) {
|
1312
|
-
// NOTE: addresults will call processRestOfRun when completed
|
1353
|
+
// NOTE: addresults will call processRestOfRun when completed.
|
1313
1354
|
x.runs[aci].addResults();
|
1314
1355
|
} else {
|
1315
1356
|
// Do not add results...
|
1316
1357
|
UI.warn(`Model run #${aci} incomplete -- results will be invalid`);
|
1317
|
-
// ... but do perform the usual post-processing
|
1318
|
-
// NOTE:
|
1358
|
+
// ... but do perform the usual post-processing.
|
1359
|
+
// NOTE: When sensitivity analysis is being performed, switch back to SA.
|
1319
1360
|
if(SENSITIVITY_ANALYSIS.experiment) {
|
1320
1361
|
SENSITIVITY_ANALYSIS.processRestOfRun();
|
1321
1362
|
} else {
|
@@ -72,18 +72,18 @@ class ActorManager {
|
|
72
72
|
}
|
73
73
|
|
74
74
|
roundLetter(n) {
|
75
|
-
//
|
76
|
-
// NOTE:
|
77
|
-
if(n < 1 || n >
|
75
|
+
// Return integer `n` as lower case letter: 1 = a, 2 = b, 26 = z.
|
76
|
+
// NOTE: Numbers 27-31 return upper case A-E; beyond ranges results in '?'.
|
77
|
+
if(n < 1 || n > VM.max_rounds) return '?';
|
78
78
|
return VM.round_letters[n];
|
79
79
|
}
|
80
80
|
|
81
81
|
checkRoundSequence(s) {
|
82
82
|
// Expects a string with zero or more round letters
|
83
|
-
for(
|
84
|
-
const n = VM.round_letters.indexOf(
|
83
|
+
for(const rl of s) {
|
84
|
+
const n = VM.round_letters.indexOf(rl);
|
85
85
|
if(n < 1 || n > this.rounds) {
|
86
|
-
UI.warn(`Round ${
|
86
|
+
UI.warn(`Round ${rl} outside range (a` +
|
87
87
|
(this.rounds > 1 ? '-' + this.roundLetter(this.rounds) : '') + ')');
|
88
88
|
return false;
|
89
89
|
}
|
@@ -160,20 +160,14 @@ class ActorManager {
|
|
160
160
|
// in the actors table, but both need to be passed on.
|
161
161
|
const p = event.target.parentElement;
|
162
162
|
// Pass name and weight of the selected actor (first and second
|
163
|
-
// TD of this TR)
|
163
|
+
// TD of this TR).
|
164
164
|
ACTOR_MANAGER.showEditActorDialog(
|
165
165
|
p.cells[0].innerText, p.cells[1].innerText);
|
166
166
|
};
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
for(let i = 0; i < abns.length; i++) {
|
172
|
-
abns[i].addEventListener('click', eaf);
|
173
|
-
}
|
174
|
-
for(let i = 0; i < abws.length; i++) {
|
175
|
-
abws[i].addEventListener('click', eaf);
|
176
|
-
}
|
167
|
+
for(const ab of abs) ab.addEventListener('click', abf);
|
168
|
+
// Clicking the other cells should open the ACTOR dialog.
|
169
|
+
for(const abn of abns) abn.addEventListener('click', eaf);
|
170
|
+
for(const abw of abws) abw.addEventListener('click', eaf);
|
177
171
|
UI.modals.actors.show();
|
178
172
|
}
|
179
173
|
|
@@ -190,7 +184,7 @@ class ActorManager {
|
|
190
184
|
}
|
191
185
|
|
192
186
|
addRound() {
|
193
|
-
// Limit # rounds to
|
187
|
+
// Limit # rounds to 31 to cope with 32 bit integer used by JavaScript.
|
194
188
|
if(this.rounds < VM.max_rounds) {
|
195
189
|
this.rounds++;
|
196
190
|
this.round_count.innerHTML = pluralS(this.rounds, 'round');
|
@@ -202,12 +196,12 @@ class ActorManager {
|
|
202
196
|
if(this.selected_round > 0 && this.selected_round <= this.rounds) {
|
203
197
|
const mask = Math.pow(2, this.selected_round) - 1;
|
204
198
|
this.updateRoundFlags();
|
205
|
-
for(
|
206
|
-
let rf =
|
199
|
+
for(const a of MODEL.actor_list) {
|
200
|
+
let rf = a[2];
|
207
201
|
const
|
208
202
|
low = (rf & mask),
|
209
203
|
high = (rf & ~mask) >>> 1;
|
210
|
-
|
204
|
+
a[2] = (low | high);
|
211
205
|
}
|
212
206
|
this.rounds--;
|
213
207
|
this.selected_round = 0;
|
@@ -291,16 +285,13 @@ class ActorManager {
|
|
291
285
|
}
|
292
286
|
|
293
287
|
updateActorProperties() {
|
294
|
-
// This method is called when the modeler clicks OK on the actor list dialog
|
288
|
+
// This method is called when the modeler clicks OK on the actor list dialog.
|
295
289
|
this.updateRoundFlags();
|
296
290
|
const xp = new ExpressionParser('');
|
297
|
-
let
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
ali = MODEL.actor_list[i];
|
302
|
-
a = MODEL.actors[ali[0]];
|
303
|
-
// Rename actor if name has been changed
|
291
|
+
let ok = true;
|
292
|
+
for(const ali of MODEL.actor_list) {
|
293
|
+
const a = MODEL.actors[ali[0]];
|
294
|
+
// Rename actor if name has been changed.
|
304
295
|
if(a.displayName != ali[1]) a.rename(ali[1]);
|
305
296
|
// Set its round flags
|
306
297
|
a.round_flags = ali[2];
|
@@ -316,7 +307,7 @@ class ActorManager {
|
|
316
307
|
a.weight.update(xp);
|
317
308
|
}
|
318
309
|
}
|
319
|
-
// Update import/export status
|
310
|
+
// Update import/export status.
|
320
311
|
MODEL.ioUpdate(a, ali[4]);
|
321
312
|
}
|
322
313
|
const seq = this.sequence.value;
|
@@ -329,8 +320,8 @@ class ActorManager {
|
|
329
320
|
}
|
330
321
|
|
331
322
|
showActorInfo(n, shift) {
|
332
|
-
// Show actor documentation when Shift is held down
|
333
|
-
// NOTE: do not allow documentation of "(no actor)"
|
323
|
+
// Show actor documentation when Shift is held down.
|
324
|
+
// NOTE: do not allow documentation of "(no actor)".
|
334
325
|
if(n > 0) {
|
335
326
|
const a = MODEL.actorByID(MODEL.actor_list[n][0]);
|
336
327
|
DOCUMENTATION_MANAGER.update(a, shift);
|
@@ -338,4 +329,3 @@ class ActorManager {
|
|
338
329
|
}
|
339
330
|
|
340
331
|
} // END of class ActorManager
|
341
|
-
|