archaeopteryx 3.15.0 → 3.16.0

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/forester.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- // v 3.15.0
23
+ // v 3.16.0
24
24
  // 2026-09-17
25
25
  //
26
26
  // forester.js is a general suite for dealing with phylogenetic trees.
@@ -3901,46 +3901,208 @@
3901
3901
  // A uniform grid rather than all-pairs: a 13,246-tip tree with branch
3902
3902
  // lengths on offers 18,511 boxes, and 171 million comparisons per redraw
3903
3903
  // is not a redraw.
3904
- forester.labelOccupancy = function (cell) {
3904
+ /**
3905
+ * The same first-come space-keeping as labelOccupancy, but for boxes that
3906
+ * are TURNED: each claim is a quadrilateral, and two of them overlap only
3907
+ * if their own outlines do.
3908
+ *
3909
+ * A turned label cannot use the axis-aligned map. The bounds of a rotated
3910
+ * rectangle grow with its length, so a long name lying near its spoke
3911
+ * reserves several times its own area, and the map refuses neighbours that
3912
+ * a reader can see are clear of it. Measured on a 48-label circular ring:
3913
+ * 28 pairs "overlapping" by the bounds where the outlines gave 0, and a
3914
+ * first-come pass over the same labels in the same order kept 48 by
3915
+ * outline against 32 by bounds -- sixteen names dropped for nothing.
3916
+ *
3917
+ * The grid is still used, on each quad's bounds, to find which claims are
3918
+ * worth testing; only the test itself is exact.
3919
+ *
3920
+ * @param cell - grid size, in the same units as the quads
3921
+ * @returns an object with claim(quad) and occupy(quad); a quad is four
3922
+ * [x, y] corners in order round the shape
3923
+ */
3924
+ forester.orientedOccupancy = function (cell) {
3905
3925
  let size = Math.max(4, cell);
3906
3926
  let cells = new Map();
3907
- return {
3908
- // true when nothing already granted overlaps, and then recorded
3909
- claim: function (x, y, w, h) {
3910
- if (!(w > 0) || !(h > 0)) {
3911
- return true; // reserves no space; recording it would only cost comparisons
3927
+ let boundsOf = function (quad) {
3928
+ let x0 = quad[0][0], x1 = quad[0][0], y0 = quad[0][1], y1 = quad[0][1];
3929
+ for (let i = 1; i < quad.length; ++i) {
3930
+ x0 = Math.min(x0, quad[i][0]);
3931
+ x1 = Math.max(x1, quad[i][0]);
3932
+ y0 = Math.min(y0, quad[i][1]);
3933
+ y1 = Math.max(y1, quad[i][1]);
3934
+ }
3935
+ return [x0, y0, x1, y1];
3936
+ };
3937
+ // Separating-axis test. A rectangle's own two edge normals are enough
3938
+ // for itself, but BOTH shapes' normals are needed: with only one
3939
+ // shape's, a box that clears another along the OTHER's axis is
3940
+ // reported as overlapping. Strict, so shapes that merely touch do not
3941
+ // overlap -- as in labelOccupancy.
3942
+ let overlaps = function (P, Q) {
3943
+ for (let s = 0; s < 2; ++s) {
3944
+ let R = s === 0 ? P : Q;
3945
+ for (let k = 0; k < 2; ++k) {
3946
+ let ax = R[k + 1][1] - R[k][1];
3947
+ let ay = R[k][0] - R[k + 1][0];
3948
+ let p0 = Infinity, p1 = -Infinity, q0 = Infinity, q1 = -Infinity;
3949
+ for (let i = 0; i < P.length; ++i) {
3950
+ let d = (P[i][0] * ax) + (P[i][1] * ay);
3951
+ p0 = Math.min(p0, d);
3952
+ p1 = Math.max(p1, d);
3953
+ }
3954
+ for (let i = 0; i < Q.length; ++i) {
3955
+ let d = (Q[i][0] * ax) + (Q[i][1] * ay);
3956
+ q0 = Math.min(q0, d);
3957
+ q1 = Math.max(q1, d);
3958
+ }
3959
+ if (p1 <= q0 || q1 <= p0) {
3960
+ return false;
3961
+ }
3912
3962
  }
3913
- let c0 = Math.floor(x / size), c1 = Math.floor((x + w) / size);
3914
- let r0 = Math.floor(y / size), r1 = Math.floor((y + h) / size);
3915
- for (let c = c0; c <= c1; ++c) {
3916
- for (let r = r0; r <= r1; ++r) {
3917
- let bucket = cells.get(c + ':' + r);
3918
- if (!bucket) {
3963
+ }
3964
+ return true;
3965
+ };
3966
+ let record = function (quad, b) {
3967
+ let c1 = Math.floor(b[2] / size);
3968
+ let r1 = Math.floor(b[3] / size);
3969
+ for (let c = Math.floor(b[0] / size); c <= c1; ++c) {
3970
+ for (let r = Math.floor(b[1] / size); r <= r1; ++r) {
3971
+ let key = c + ':' + r;
3972
+ let bucket = cells.get(key);
3973
+ if (!bucket) {
3974
+ bucket = [];
3975
+ cells.set(key, bucket);
3976
+ }
3977
+ bucket.push(quad);
3978
+ }
3979
+ }
3980
+ };
3981
+ let free = function (quad, b) {
3982
+ let seen = null;
3983
+ let c1 = Math.floor(b[2] / size);
3984
+ let r1 = Math.floor(b[3] / size);
3985
+ for (let c = Math.floor(b[0] / size); c <= c1; ++c) {
3986
+ for (let r = Math.floor(b[1] / size); r <= r1; ++r) {
3987
+ let bucket = cells.get(c + ':' + r);
3988
+ if (!bucket) {
3989
+ continue;
3990
+ }
3991
+ for (let i = 0; i < bucket.length; ++i) {
3992
+ // a quad spans several cells, so the same one comes up
3993
+ // more than once; the test is the expensive part
3994
+ if (seen === null) {
3995
+ seen = new Set();
3996
+ }
3997
+ if (seen.has(bucket[i])) {
3919
3998
  continue;
3920
3999
  }
3921
- for (let i = 0; i < bucket.length; ++i) {
3922
- let b = bucket[i];
3923
- // strict: boxes that merely touch do not overlap
3924
- if (x < (b[0] + b[2]) && (x + w) > b[0]
3925
- && y < (b[1] + b[3]) && (y + h) > b[1]) {
3926
- return false;
3927
- }
4000
+ seen.add(bucket[i]);
4001
+ if (overlaps(quad, bucket[i])) {
4002
+ return false;
3928
4003
  }
3929
4004
  }
3930
4005
  }
3931
- let box = [x, y, w, h];
3932
- for (let c = c0; c <= c1; ++c) {
3933
- for (let r = r0; r <= r1; ++r) {
3934
- let key = c + ':' + r;
3935
- let bucket = cells.get(key);
3936
- if (!bucket) {
3937
- bucket = [];
3938
- cells.set(key, bucket);
4006
+ }
4007
+ return true;
4008
+ };
4009
+ let degenerate = function (quad) {
4010
+ if (!quad || quad.length !== 4) {
4011
+ return true;
4012
+ }
4013
+ let b = boundsOf(quad);
4014
+ return !((b[2] - b[0]) > 0) || !((b[3] - b[1]) > 0);
4015
+ };
4016
+ return {
4017
+ claim: function (quad) {
4018
+ if (degenerate(quad)) {
4019
+ return true; // reserves nothing, as a zero-size box does
4020
+ }
4021
+ let b = boundsOf(quad);
4022
+ if (!free(quad, b)) {
4023
+ return false;
4024
+ }
4025
+ record(quad, b);
4026
+ return true;
4027
+ },
4028
+ // record without asking, for what is drawn whatever else is there
4029
+ occupy: function (quad) {
4030
+ if (degenerate(quad)) {
4031
+ return true;
4032
+ }
4033
+ let b = boundsOf(quad);
4034
+ let clear = free(quad, b);
4035
+ record(quad, b);
4036
+ return clear;
4037
+ }
4038
+ };
4039
+ };
4040
+
4041
+ forester.labelOccupancy = function (cell) {
4042
+ let size = Math.max(4, cell);
4043
+ let cells = new Map();
4044
+ let record = function (x, y, w, h) {
4045
+ let box = [x, y, w, h];
4046
+ let c1 = Math.floor((x + w) / size);
4047
+ let r1 = Math.floor((y + h) / size);
4048
+ for (let c = Math.floor(x / size); c <= c1; ++c) {
4049
+ for (let r = Math.floor(y / size); r <= r1; ++r) {
4050
+ let key = c + ':' + r;
4051
+ let bucket = cells.get(key);
4052
+ if (!bucket) {
4053
+ bucket = [];
4054
+ cells.set(key, bucket);
4055
+ }
4056
+ bucket.push(box);
4057
+ }
4058
+ }
4059
+ };
4060
+ let free = function (x, y, w, h) {
4061
+ let c1 = Math.floor((x + w) / size);
4062
+ let r1 = Math.floor((y + h) / size);
4063
+ for (let c = Math.floor(x / size); c <= c1; ++c) {
4064
+ for (let r = Math.floor(y / size); r <= r1; ++r) {
4065
+ let bucket = cells.get(c + ':' + r);
4066
+ if (!bucket) {
4067
+ continue;
4068
+ }
4069
+ for (let i = 0; i < bucket.length; ++i) {
4070
+ let b = bucket[i];
4071
+ // strict: boxes that merely touch do not overlap
4072
+ if (x < (b[0] + b[2]) && (x + w) > b[0]
4073
+ && y < (b[1] + b[3]) && (y + h) > b[1]) {
4074
+ return false;
3939
4075
  }
3940
- bucket.push(box);
3941
4076
  }
3942
4077
  }
4078
+ }
4079
+ return true;
4080
+ };
4081
+ return {
4082
+ // true when nothing already granted overlaps, and then recorded
4083
+ claim: function (x, y, w, h) {
4084
+ if (!(w > 0) || !(h > 0)) {
4085
+ return true; // reserves no space; recording it would only cost comparisons
4086
+ }
4087
+ if (!free(x, y, w, h)) {
4088
+ return false;
4089
+ }
4090
+ record(x, y, w, h);
3943
4091
  return true;
4092
+ },
4093
+ // Record without asking, for what is drawn whatever else is there.
4094
+ // A refused CLAIM records nothing, so a label put through claim()
4095
+ // and refused would leave the space open and let a number print
4096
+ // straight through the name it lost to -- the opposite of the
4097
+ // point. Returns whether the space was clear, for measurement;
4098
+ // the box is recorded either way.
4099
+ occupy: function (x, y, w, h) {
4100
+ if (!(w > 0) || !(h > 0)) {
4101
+ return true;
4102
+ }
4103
+ let clear = free(x, y, w, h);
4104
+ record(x, y, w, h);
4105
+ return clear;
3944
4106
  }
3945
4107
  };
3946
4108
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archaeopteryx",
3
- "version": "3.15.0",
3
+ "version": "3.16.0",
4
4
  "description": "Archaeopteryx.js is a software tool for the visualization and analysis of highly annotated phylogenetic trees.",
5
5
  "main": "archaeopteryx.js",
6
6
  "types": "archaeopteryx.d.ts",
@@ -25,7 +25,9 @@
25
25
  "lint:fix": "eslint . --fix",
26
26
  "format": "prettier --write .",
27
27
  "format:check": "prettier --check .",
28
- "docs:sync": "cp archaeopteryx.js forester.js docs/lib/"
28
+ "docs:sync": "cp archaeopteryx.js forester.js docs/lib/",
29
+ "test:browser": "node test/browser/run.js",
30
+ "test:browser:quick": "node test/browser/run.js --quick"
29
31
  },
30
32
  "repository": {
31
33
  "type": "git",