@mark1russell7/packing 0.2.12 → 0.2.14
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/build/index.d.ts +82 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +224 -66
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +15 -0
- package/src/maxrects.ts +44 -56
- package/src/registry.ts +3 -2
- package/src/select.ts +177 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/packing",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.14",
|
|
5
5
|
"description": "Rectangle packing: shelf, guillotine and skyline strategies, push cascades and measures of a layout.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"rectangle-packing",
|
package/src/index.ts
CHANGED
|
@@ -103,6 +103,21 @@ export {
|
|
|
103
103
|
} from "./portfolio-cell.ts";
|
|
104
104
|
export { PORTFOLIO_SETS } from "./portfolio-sets.ts";
|
|
105
105
|
export { portfolioStrategy, type PortfolioStrategyOptions } from "./portfolio-strategy.ts";
|
|
106
|
+
export {
|
|
107
|
+
checkSelectTable,
|
|
108
|
+
SELECT_MAX_DEPTH,
|
|
109
|
+
SELECT_MEMBERS,
|
|
110
|
+
SELECT_STAGE,
|
|
111
|
+
SELECT_TABLE,
|
|
112
|
+
selectFeatures,
|
|
113
|
+
selectLeaf,
|
|
114
|
+
selectStrategy,
|
|
115
|
+
type SelectFeatures,
|
|
116
|
+
type SelectFlagFeature,
|
|
117
|
+
type SelectNode,
|
|
118
|
+
type SelectNumberFeature,
|
|
119
|
+
type SelectStrategyOptions,
|
|
120
|
+
} from "./select.ts";
|
|
106
121
|
export type { Portfolio, PortfolioPick, PortfolioTrace, MemberTrace, SelectLevel } from "@mark1russell7/layout-contract";
|
|
107
122
|
export { checkHard, type ContainerTest, type PackHard } from "./check.ts";
|
|
108
123
|
export {
|
package/src/maxrects.ts
CHANGED
|
@@ -89,19 +89,41 @@ function inside(a: Box, b: Box): boolean {
|
|
|
89
89
|
*/
|
|
90
90
|
function cut(free: Box[], b: Box, minW: number, minH: number): { free: Box[]; hits: number } {
|
|
91
91
|
const kept: Box[] = [];
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
const
|
|
92
|
+
// A part spans the box on one axis and touches the box on the other axis. Thus a part of one side is not in a part of
|
|
93
|
+
// another side. A kept rectangle with the part in it has an edge on the same side of the box.
|
|
94
|
+
const near: Box[][] = [[], [], [], []];
|
|
95
|
+
// For each side of the box (0 left, 1 right, 2 top, 3 bottom), the parts that stay so far. Each part has five numbers
|
|
96
|
+
// in the list: its four edges and its index in the order of creation.
|
|
97
|
+
const stay: number[][] = [[], [], [], []];
|
|
98
|
+
let made = 0;
|
|
95
99
|
let hits = 0;
|
|
100
|
+
// A part stays when no other part holds more than it, and no equal part comes before it. This rule does not use an order.
|
|
101
|
+
// Thus a new part goes when a part that stays holds it. Else it removes each part that it holds.
|
|
96
102
|
const add = (side: number, x0: number, y0: number, x1: number, y1: number): void => {
|
|
97
|
-
if (x1 - x0
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
if (x1 - x0 < minW || y1 - y0 < minH) return;
|
|
104
|
+
const own = stay[side]!;
|
|
105
|
+
for (let k = 0; k < own.length; ) {
|
|
106
|
+
const qx0 = own[k]!;
|
|
107
|
+
const qy0 = own[k + 1]!;
|
|
108
|
+
const qx1 = own[k + 2]!;
|
|
109
|
+
const qy1 = own[k + 3]!;
|
|
110
|
+
if (x0 >= qx0 && y0 >= qy0 && x1 <= qx1 && y1 <= qy1) return;
|
|
111
|
+
if (qx0 >= x0 && qy0 >= y0 && qx1 <= x1 && qy1 <= y1) {
|
|
112
|
+
// The last part of the list takes the place of the removed part
|
|
113
|
+
const last = own.length - 5;
|
|
114
|
+
if (k !== last) for (let n = 0; n < 5; n++) own[k + n] = own[last + n]!;
|
|
115
|
+
own.length = last;
|
|
116
|
+
} else k += 5;
|
|
100
117
|
}
|
|
118
|
+
own.push(x0, y0, x1, y1, made++);
|
|
101
119
|
};
|
|
102
120
|
for (const f of free) {
|
|
103
121
|
if (b.x0 >= f.x1 || b.x1 <= f.x0 || b.y0 >= f.y1 || b.y1 <= f.y0) {
|
|
104
122
|
kept.push(f);
|
|
123
|
+
if (f.x1 === b.x0) near[0]!.push(f);
|
|
124
|
+
if (f.x0 === b.x1) near[1]!.push(f);
|
|
125
|
+
if (f.y1 === b.y0) near[2]!.push(f);
|
|
126
|
+
if (f.y0 === b.y1) near[3]!.push(f);
|
|
105
127
|
continue;
|
|
106
128
|
}
|
|
107
129
|
hits++;
|
|
@@ -110,62 +132,28 @@ function cut(free: Box[], b: Box, minW: number, minH: number): { free: Box[]; hi
|
|
|
110
132
|
if (b.y0 > f.y0) add(2, f.x0, f.y0, f.x1, b.y0);
|
|
111
133
|
if (b.y1 < f.y1) add(3, f.x0, b.y1, f.x1, f.y1);
|
|
112
134
|
}
|
|
113
|
-
if (
|
|
114
|
-
// Each part is in the free rectangle that it comes from. Thus
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const gone = new Uint8Array(parts.length);
|
|
128
|
-
const stay: Box[][] = [[], [], [], []];
|
|
129
|
-
for (const i of order) {
|
|
130
|
-
const p = parts[i]!;
|
|
131
|
-
const own = stay[sides[i]!]!;
|
|
132
|
-
let drop = false;
|
|
133
|
-
for (const q of own) {
|
|
134
|
-
if (inside(p, q)) {
|
|
135
|
-
drop = true;
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
if (drop) gone[i] = 1;
|
|
140
|
-
else own.push(p);
|
|
141
|
-
}
|
|
142
|
-
for (let i = 0; i < parts.length; i++) {
|
|
143
|
-
if (gone[i] === 1) continue;
|
|
144
|
-
const p = parts[i]!;
|
|
145
|
-
let drop = false;
|
|
146
|
-
for (const k of near[sides[i]!]!) {
|
|
147
|
-
if (inside(p, k)) {
|
|
148
|
-
drop = true;
|
|
149
|
-
break;
|
|
135
|
+
if (made === 0) return { free: kept, hits };
|
|
136
|
+
// Each part is in the free rectangle that it comes from. Thus no kept rectangle is in a part.
|
|
137
|
+
// The parts that stay go to the list in the order of creation.
|
|
138
|
+
const out: (Box | undefined)[] = new Array<Box | undefined>(made).fill(undefined);
|
|
139
|
+
for (let side = 0; side < 4; side++) {
|
|
140
|
+
const own = stay[side]!;
|
|
141
|
+
for (let k = 0; k < own.length; k += 5) {
|
|
142
|
+
const p: Box = { x0: own[k]!, y0: own[k + 1]!, x1: own[k + 2]!, y1: own[k + 3]! };
|
|
143
|
+
let drop = false;
|
|
144
|
+
for (const q of near[side]!) {
|
|
145
|
+
if (inside(p, q)) {
|
|
146
|
+
drop = true;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
150
149
|
}
|
|
150
|
+
if (!drop) out[own[k + 4]!] = p;
|
|
151
151
|
}
|
|
152
|
-
if (!drop) kept.push(p);
|
|
153
152
|
}
|
|
153
|
+
for (const p of out) if (p !== undefined) kept.push(p);
|
|
154
154
|
return { free: kept, hits };
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
/**
|
|
158
|
-
* This function orders two boxes. The smaller `x0` comes first, then the smaller `y0`, then the larger `x1`, then the
|
|
159
|
-
* larger `y1`. Thus a box comes before each other box in it.
|
|
160
|
-
*/
|
|
161
|
-
function byExtent(a: Box, b: Box): number {
|
|
162
|
-
if (a.x0 !== b.x0) return a.x0 < b.x0 ? -1 : 1;
|
|
163
|
-
if (a.y0 !== b.y0) return a.y0 < b.y0 ? -1 : 1;
|
|
164
|
-
if (a.x1 !== b.x1) return a.x1 > b.x1 ? -1 : 1;
|
|
165
|
-
if (a.y1 !== b.y1) return a.y1 > b.y1 ? -1 : 1;
|
|
166
|
-
return 0;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
157
|
/** The edges of the boxes on one axis: for each coordinate, the flat list of the ranges on the other axis. */
|
|
170
158
|
type Edges = Map<number, number[]>;
|
|
171
159
|
|
package/src/registry.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { maxrects, push, rows, shelf, skyline } from "./adapters.ts";
|
|
|
2
2
|
import { aspectFit } from "./aspect-fit.ts";
|
|
3
3
|
import type { PackInput, PackOutput } from "./contract.ts";
|
|
4
4
|
import { portfolioStrategy } from "./portfolio-strategy.ts";
|
|
5
|
+
import { selectStrategy } from "./select.ts";
|
|
5
6
|
import { createContext, type PackContext, type Strategy, type Supports } from "./strategy.ts";
|
|
6
7
|
|
|
7
8
|
/** Each single strategy of the package. The benchmark starts each one on each input of the corpus. */
|
|
@@ -51,9 +52,9 @@ export const aspectFitStrategy: Strategy<AspectFitStrategyOptions> = {
|
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* The composite strategies of the registry: each one packs an input with other strategies. They are not in `strategies`.
|
|
54
|
-
* Since 0.2.7, the list has `portfolio` of roadmap item k20. Roadmap item k62 adds `aspect-fit`.
|
|
55
|
+
* Since 0.2.7, the list has `portfolio` of roadmap item k20. Roadmap item k62 adds `aspect-fit`, and roadmap item k66 adds `select`.
|
|
55
56
|
*/
|
|
56
|
-
export const composites: readonly Strategy[] = Object.freeze([portfolioStrategy, aspectFitStrategy] as Strategy[]);
|
|
57
|
+
export const composites: readonly Strategy[] = Object.freeze([portfolioStrategy, aspectFitStrategy, selectStrategy] as Strategy[]);
|
|
57
58
|
|
|
58
59
|
/** This function gives the strategy with an id, also a composite strategy. It throws a `RangeError` for an id that the registry does not have. */
|
|
59
60
|
export function strategy(id: string): Strategy {
|
package/src/select.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// The strategy `select` of roadmap item k66, part 2 (`docs/PACKER.md`, "The feature select"). It reads cheap features of
|
|
2
|
+
// the input, walks a small decision table and packs with exactly one member. Nothing default uses it.
|
|
3
|
+
|
|
4
|
+
import { maxrects, rows } from "./adapters.ts";
|
|
5
|
+
import type { PackInput, PackOutput } from "./contract.ts";
|
|
6
|
+
import { round3 } from "./layout-input.ts";
|
|
7
|
+
import { spreadOf } from "./portfolio-cell.ts";
|
|
8
|
+
import type { PortfolioEntry } from "./portfolio.ts";
|
|
9
|
+
import { packWith, strategy } from "./registry.ts";
|
|
10
|
+
import type { PackContext, Strategy, Supports } from "./strategy.ts";
|
|
11
|
+
|
|
12
|
+
/** The cheap features of an input for `select`. One pass over the items and the obstacles gives them. */
|
|
13
|
+
export interface SelectFeatures {
|
|
14
|
+
/** The count of the items */
|
|
15
|
+
count: number;
|
|
16
|
+
/** The value `true` when the container has the size `fixed`. A free width, a free height or a free size gives `false`. */
|
|
17
|
+
fixed: boolean;
|
|
18
|
+
/** The value `true` when the container is a rectangle with no obstacle of another shape: the `containerShape` `rect` of `cellOf` */
|
|
19
|
+
rect: boolean;
|
|
20
|
+
/** The size spread: `spreadOf`, the standard deviation of the natural log of `sqrt(w * h)` of each item */
|
|
21
|
+
spread: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A feature of `SelectFeatures` with a number value. A node of this feature compares the value with `below`. */
|
|
25
|
+
export type SelectNumberFeature = "count" | "spread";
|
|
26
|
+
|
|
27
|
+
/** A feature of `SelectFeatures` with a flag value. */
|
|
28
|
+
export type SelectFlagFeature = "fixed" | "rect";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A node of the decision table of `select`.
|
|
32
|
+
*
|
|
33
|
+
* - A leaf has `leaf`, its number for the trace, and `pick`, the index of its member in the option `members`.
|
|
34
|
+
* - A number node goes to `yes` when the feature value is below `below`, else to `no`.
|
|
35
|
+
* - A flag node goes to `yes` when the feature value is `true`, else to `no`.
|
|
36
|
+
*/
|
|
37
|
+
export type SelectNode =
|
|
38
|
+
| { readonly leaf: number; readonly pick: number }
|
|
39
|
+
| { readonly feature: SelectNumberFeature; readonly below: number; readonly yes: SelectNode; readonly no: SelectNode }
|
|
40
|
+
| { readonly feature: SelectFlagFeature; readonly yes: SelectNode; readonly no: SelectNode };
|
|
41
|
+
|
|
42
|
+
/** The largest count of the tests on a path of a table of `select` */
|
|
43
|
+
export const SELECT_MAX_DEPTH = 3;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The default table of `select`, for the default members `rows` (0) and `maxrects-bl` (1). Roadmap item k66 fitted it on
|
|
47
|
+
* the records of `pack-screen-v1` (CI job 40675490363) and `pack-free-v1` (CI job 40685309195). A win is a fill ratio of 1.05 or
|
|
48
|
+
* more with no more hard breaks. The leaf picks the member with the most wins.
|
|
49
|
+
*
|
|
50
|
+
* - Each threshold is in a plateau of the training accuracy. The table takes a value in the plateau that other code uses.
|
|
51
|
+
* - The spread thresholds are `SPREAD_THRESHOLDS`: 0.02 and 0.27. The spread plateaus are 0 to 0.11 and 0.24 to 0.35.
|
|
52
|
+
* - The count threshold 317 is the end of the band `100` of `countCell`. Any value from 101 to 1000 gives the same split.
|
|
53
|
+
*/
|
|
54
|
+
export const SELECT_TABLE: SelectNode = Object.freeze({
|
|
55
|
+
feature: "fixed",
|
|
56
|
+
// A container of fixed size
|
|
57
|
+
yes: {
|
|
58
|
+
feature: "count",
|
|
59
|
+
below: 317,
|
|
60
|
+
// 316 items or fewer. Identical sizes: rows (0 wins of 15). Other sizes: maxrects-bl (635 wins of 850).
|
|
61
|
+
yes: { feature: "spread", below: 0.02, yes: { leaf: 0, pick: 0 }, no: { leaf: 1, pick: 1 } },
|
|
62
|
+
// 317 items or more. A uniform spread: rows (10 wins of 210). A heavy tail: maxrects-bl (184 wins of 210).
|
|
63
|
+
no: { feature: "spread", below: 0.27, yes: { leaf: 2, pick: 0 }, no: { leaf: 3, pick: 1 } },
|
|
64
|
+
},
|
|
65
|
+
// A container of free size
|
|
66
|
+
no: {
|
|
67
|
+
feature: "spread",
|
|
68
|
+
below: 0.27,
|
|
69
|
+
// Identical or uniform sizes: rows (49 wins of 490)
|
|
70
|
+
yes: { leaf: 4, pick: 0 },
|
|
71
|
+
// A heavy tail. A rectangle: rows (7 wins of 50). Another shape: maxrects-bl (172 wins of 200).
|
|
72
|
+
no: { feature: "rect", yes: { leaf: 5, pick: 0 }, no: { leaf: 6, pick: 1 } },
|
|
73
|
+
},
|
|
74
|
+
} satisfies SelectNode) as SelectNode;
|
|
75
|
+
|
|
76
|
+
/** The default members of `select`: `rows`, then `maxrects` with the rule `bl` */
|
|
77
|
+
export const SELECT_MEMBERS: readonly PortfolioEntry[] = Object.freeze([
|
|
78
|
+
{ id: "rows", strategy: "rows" },
|
|
79
|
+
{ id: "maxrects-bl", strategy: "maxrects", options: { rule: "bl" } },
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
/** The id of the trace stage of `select`. Its measure `move.select` is 1 when the pick is not member 0. */
|
|
83
|
+
export const SELECT_STAGE = "select";
|
|
84
|
+
|
|
85
|
+
/** This function gives the cheap features of an input. The spread is `spreadOf`. */
|
|
86
|
+
export function selectFeatures(input: PackInput): SelectFeatures {
|
|
87
|
+
const c = input.container;
|
|
88
|
+
const rect = (c.shape === undefined || c.shape.kind === "rect") && !(input.obstacles ?? []).some((o) => o.region.kind !== "rect");
|
|
89
|
+
return { count: input.items.length, fixed: c.fixed === "size", rect, spread: spreadOf(input) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** This function gives the leaf of the features in a table. */
|
|
93
|
+
export function selectLeaf(features: SelectFeatures, table: SelectNode = SELECT_TABLE): { leaf: number; pick: number } {
|
|
94
|
+
let node = table;
|
|
95
|
+
while (!("leaf" in node)) {
|
|
96
|
+
const go = "below" in node ? features[node.feature] < node.below : features[node.feature];
|
|
97
|
+
node = go ? node.yes : node.no;
|
|
98
|
+
}
|
|
99
|
+
return { leaf: node.leaf, pick: node.pick };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* This function checks a table against a count of members. A path has at most `SELECT_MAX_DEPTH` tests, thus the table has 8 leaves or fewer.
|
|
104
|
+
* It throws a `RangeError` for a longer path, a pick that is not a member index and a threshold that is not a number.
|
|
105
|
+
*/
|
|
106
|
+
export function checkSelectTable(table: SelectNode, members: number): void {
|
|
107
|
+
const walk = (node: SelectNode, depth: number): void => {
|
|
108
|
+
if ("leaf" in node) {
|
|
109
|
+
if (!Number.isInteger(node.pick) || node.pick < 0 || node.pick >= members) throw new RangeError(`The leaf ${node.leaf} of the table of select picks ${node.pick}, but the members are 0 to ${members - 1}.`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (depth >= SELECT_MAX_DEPTH) throw new RangeError(`The table of select has a path with more than ${SELECT_MAX_DEPTH} tests.`);
|
|
113
|
+
if ((node.feature === "count" || node.feature === "spread") && !Number.isFinite(node.below)) throw new RangeError(`The test of ${node.feature} of the table of select needs a number below.`);
|
|
114
|
+
walk(node.yes, depth + 1);
|
|
115
|
+
walk(node.no, depth + 1);
|
|
116
|
+
};
|
|
117
|
+
walk(table, 0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The options of the strategy `select` */
|
|
121
|
+
export interface SelectStrategyOptions {
|
|
122
|
+
/** The members. The default is `SELECT_MEMBERS`. A member is a strategy id, or a `PortfolioEntry` with options. */
|
|
123
|
+
members?: readonly (string | PortfolioEntry)[];
|
|
124
|
+
/** The decision table. The default is `SELECT_TABLE`. Each pick is an index of `members`. */
|
|
125
|
+
table?: SelectNode;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** This function gives the union of the supports of the default members. */
|
|
129
|
+
function unionSupports(): Supports {
|
|
130
|
+
const out: Record<string, string[]> = {};
|
|
131
|
+
for (const s of [rows, maxrects] as Strategy[]) {
|
|
132
|
+
for (const [dim, values] of Object.entries(s.supports) as [string, string[]][]) {
|
|
133
|
+
const list = (out[dim] ??= []);
|
|
134
|
+
for (const v of values) if (!list.includes(v)) list.push(v);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out as unknown as Supports;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The strategy `select`: it gives the features of the input to the table, and it packs with the one member of the leaf.
|
|
142
|
+
*
|
|
143
|
+
* - The seed of the context goes to the member. The output of the member does not change.
|
|
144
|
+
* - The output gets the trace stage `select` (`SELECT_STAGE`) after the stages of the member.
|
|
145
|
+
* - The stage has `move.select`, `select.member` (the index of the pick), `select.pick.<member id>` (1), `select.leaf` and each feature.
|
|
146
|
+
* - Its supports are the union of the supports of `rows` and `maxrects`. A member that does not support the cell throws its error.
|
|
147
|
+
* - The strategy throws a `RangeError` for an unknown strategy of a member and for a table that `checkSelectTable` refuses.
|
|
148
|
+
*/
|
|
149
|
+
export const selectStrategy: Strategy<SelectStrategyOptions> = {
|
|
150
|
+
id: "select",
|
|
151
|
+
family: "select",
|
|
152
|
+
catalog: [],
|
|
153
|
+
supports: unionSupports(),
|
|
154
|
+
defaults: {},
|
|
155
|
+
deterministic: true,
|
|
156
|
+
pack(input: PackInput, options: SelectStrategyOptions, context: PackContext): PackOutput {
|
|
157
|
+
const members = (options.members ?? SELECT_MEMBERS).map((m) => (typeof m === "string" ? { id: m, strategy: m } : m));
|
|
158
|
+
const table = options.table ?? SELECT_TABLE;
|
|
159
|
+
for (const m of members) strategy(m.strategy);
|
|
160
|
+
if (options.table !== undefined || options.members !== undefined) checkSelectTable(table, members.length);
|
|
161
|
+
const features = selectFeatures(input);
|
|
162
|
+
const { leaf, pick } = selectLeaf(features, table);
|
|
163
|
+
const member = members[pick]!;
|
|
164
|
+
const out = packWith(member.strategy, input, member.options, context.seed);
|
|
165
|
+
const measures: Record<string, number> = {
|
|
166
|
+
"move.select": pick === 0 ? 0 : 1,
|
|
167
|
+
"select.member": pick,
|
|
168
|
+
[`select.pick.${member.id}`]: 1,
|
|
169
|
+
"select.leaf": leaf,
|
|
170
|
+
"select.count": features.count,
|
|
171
|
+
"select.fixed": features.fixed ? 1 : 0,
|
|
172
|
+
"select.rect": features.rect ? 1 : 0,
|
|
173
|
+
"select.spread": round3(features.spread),
|
|
174
|
+
};
|
|
175
|
+
return { ...out, trace: [...(out.trace ?? []), { id: SELECT_STAGE, measures }] };
|
|
176
|
+
},
|
|
177
|
+
};
|