@field/cascade 0.5.2 → 0.7.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/assets/{CodeEditor-BHej8kM9.js → CodeEditor-DkpxJFRs.js} +7 -7
  3. package/dist/assets/{CodePanel-CT3eirK8.js → CodePanel-O31InnQg.js} +2 -2
  4. package/dist/assets/{CustomNodeDialog-BG0m7_Rv.js → CustomNodeDialog-Cn3sT-M2.js} +1 -1
  5. package/dist/assets/{ProjectSettings-BxoLWJG6.js → ProjectSettings-BzEmtsYc.js} +2 -2
  6. package/dist/assets/{SettingsDialog-r9PzZ22z.js → SettingsDialog-zWct7T8j.js} +1 -1
  7. package/dist/assets/{VersionHistoryDialog-B6zUV6Dy.js → VersionHistoryDialog-DCe6XQcO.js} +1 -1
  8. package/dist/assets/{core-wwx5881P.js → core-DwPLRUzm.js} +2 -2
  9. package/dist/assets/{cssMode-DY5T2Or0.js → cssMode-C1bjAPYj.js} +1 -1
  10. package/dist/assets/definitions-Dh-oZAA5.js +1 -0
  11. package/dist/assets/{editor.api-mkORHEMm.js → editor.api-wr5lElMs.js} +1 -1
  12. package/dist/assets/{freemarker2-CIWlk0sw.js → freemarker2-DQHTaSDb.js} +1 -1
  13. package/dist/assets/geo-DLRNQg57.js +1 -0
  14. package/dist/assets/{handlebars-t4jbj3Br.js → handlebars-DOiZYGbp.js} +1 -1
  15. package/dist/assets/{html-Ctx7UOlY.js → html-B1u4CWMY.js} +1 -1
  16. package/dist/assets/{htmlMode-BKNZxVw7.js → htmlMode-YNKJ8Hq1.js} +1 -1
  17. package/dist/assets/{image-BCBFz0gD.js → image-DmeeS7Tl.js} +3 -3
  18. package/dist/assets/{index-CeqTfMbr.js → index-C35beBgj.js} +36 -36
  19. package/dist/assets/{javascript-Da_TJBzH.js → javascript-DVujl3OI.js} +1 -1
  20. package/dist/assets/{jsonMode-Ftt5d1nV.js → jsonMode-Bp_Ujoae.js} +1 -1
  21. package/dist/assets/{liquid-D5kWYG57.js → liquid-C0wcDX3X.js} +1 -1
  22. package/dist/assets/{lspLanguageFeatures-B-93LC_L.js → lspLanguageFeatures-C2XtKL_c.js} +1 -1
  23. package/dist/assets/math-D0hLbnR2.js +1 -0
  24. package/dist/assets/{mdx-DGT3C_qs.js → mdx-Dz4Hb1ZN.js} +1 -1
  25. package/dist/assets/pop-OaOoEUwk.js +1 -0
  26. package/dist/assets/{python-Bz3Eew5S.js → python-Y4eTKlAp.js} +1 -1
  27. package/dist/assets/{razor-CFDdKpKI.js → razor-BmpH_QN_.js} +1 -1
  28. package/dist/assets/{tsMode-BaCINWBI.js → tsMode-mOQwydYU.js} +1 -1
  29. package/dist/assets/{typescript-BoOWspVs.js → typescript-8ItD7azm.js} +1 -1
  30. package/dist/assets/{workers-dSWyDxsQ.js → workers-xhda_-L2.js} +1 -1
  31. package/dist/assets/{xml-eFi1_vYt.js → xml-ydyZSquM.js} +1 -1
  32. package/dist/assets/{yaml-a7xrJHxm.js → yaml-BQjpTmW5.js} +1 -1
  33. package/dist/cli/index.js +11259 -19155
  34. package/dist/index.html +1 -1
  35. package/dist/runtime/builtins/geo/definitions.d.ts +153 -0
  36. package/dist/runtime/builtins/geo/definitions.js +75 -0
  37. package/dist/runtime/builtins/geo/index.d.ts +3 -1
  38. package/dist/runtime/builtins/geo/index.js +4 -1
  39. package/dist/runtime/builtins/index.js +2 -0
  40. package/dist/runtime/builtins/math/definitions.d.ts +845 -0
  41. package/dist/runtime/builtins/math/definitions.js +285 -0
  42. package/dist/runtime/builtins/math/index.d.ts +2 -0
  43. package/dist/runtime/builtins/math/index.js +2 -0
  44. package/dist/runtime/geometry/index.d.ts +1 -0
  45. package/dist/runtime/geometry/index.js +1 -0
  46. package/dist/runtime/geometry/rects.d.ts +88 -0
  47. package/dist/runtime/geometry/rects.js +68 -0
  48. package/doc/NODE_REFERENCE.md +141 -1
  49. package/package.json +1 -1
  50. package/dist/assets/definitions-BgCvRHyR.js +0 -1
  51. package/dist/assets/geo-CIxgs0oE.js +0 -1
  52. package/dist/assets/pop-BJGN5Ac4.js +0 -1
@@ -0,0 +1,285 @@
1
+ function asComponents(value, label) {
2
+ if (typeof value === "number")
3
+ return [value];
4
+ if (Array.isArray(value) && value.every((part) => typeof part === "number"))
5
+ return value;
6
+ if (value === undefined || value === null)
7
+ return [0];
8
+ throw new Error(`${label} needs a number or a vector of numbers, and got ${describe(value)}`);
9
+ }
10
+ function describe(value) {
11
+ if (Array.isArray(value))
12
+ return `an array of ${value.length}`;
13
+ return `a ${typeof value}`;
14
+ }
15
+ /**
16
+ * Apply an operation component-wise, broadcasting a scalar against a vector.
17
+ *
18
+ * Mismatched vector widths throw rather than padding: `vec2 + vec3` is a wiring
19
+ * mistake, and quietly treating the missing component as zero would produce a
20
+ * plausible wrong answer — the failure that is hardest to see in a picture.
21
+ */
22
+ function combine(left, right, operate) {
23
+ const a = asComponents(left, "the first input");
24
+ const b = asComponents(right, "the second input");
25
+ if (a.length !== b.length && a.length !== 1 && b.length !== 1)
26
+ throw new Error(`cannot combine a vector of ${a.length} with a vector of ${b.length}`);
27
+ const width = Math.max(a.length, b.length);
28
+ const out = [];
29
+ for (let index = 0; index < width; index += 1)
30
+ out.push(operate(a[a.length === 1 ? 0 : index], b[b.length === 1 ? 0 : index]));
31
+ return width === 1 ? out[0] : out;
32
+ }
33
+ function map(value, operate) {
34
+ const components = asComponents(value, "the input");
35
+ const out = components.map(operate);
36
+ return out.length === 1 ? out[0] : out;
37
+ }
38
+ /**
39
+ * Division and modulo by zero give **zero**, not `Infinity` or `NaN`.
40
+ *
41
+ * Both choices hide something, so this is the lesser one. A non-finite number
42
+ * flows downstream into a position and the geometry silently vanishes — a
43
+ * blank frame with no error anywhere, which is the hardest thing to diagnose
44
+ * here. And a zero divisor is almost always a parameter nobody set yet rather
45
+ * than an intent, so zero is also the likelier thing to have meant.
46
+ */
47
+ function divide(a, b) {
48
+ return b === 0 ? 0 : a / b;
49
+ }
50
+ const BINARY = {
51
+ add: (a, b) => a + b,
52
+ subtract: (a, b) => a - b,
53
+ multiply: (a, b) => a * b,
54
+ divide,
55
+ power: (a, b) => a ** b,
56
+ modulo: (a, b) => (b === 0 ? 0 : a % b),
57
+ min: (a, b) => Math.min(a, b),
58
+ max: (a, b) => Math.max(a, b),
59
+ atan2: (a, b) => Math.atan2(a, b),
60
+ };
61
+ const DEG = Math.PI / 180;
62
+ const UNARY = {
63
+ abs: Math.abs,
64
+ negate: (a) => -a,
65
+ floor: Math.floor,
66
+ ceil: Math.ceil,
67
+ round: Math.round,
68
+ sqrt: (a) => (a < 0 ? 0 : Math.sqrt(a)),
69
+ sign: Math.sign,
70
+ // Trigonometry in RADIANS, matching the expression engine, where bare `sin`
71
+ // is radians too. `radians` and `degrees` are here for the conversion, and
72
+ // the divergence from `Transform`'s degrees is recorded in AGENTS.md.
73
+ sin: Math.sin,
74
+ cos: Math.cos,
75
+ tan: Math.tan,
76
+ asin: (a) => Math.asin(Math.max(-1, Math.min(1, a))),
77
+ acos: (a) => Math.acos(Math.max(-1, Math.min(1, a))),
78
+ exp: Math.exp,
79
+ log: (a) => (a <= 0 ? 0 : Math.log(a)),
80
+ radians: (a) => a * DEG,
81
+ degrees: (a) => a / DEG,
82
+ /** The fractional part, always positive — `fract(-0.25)` is `0.75`, which is
83
+ * what a repeating pattern wants and what GLSL's `fract` does. */
84
+ fract: (a) => a - Math.floor(a),
85
+ };
86
+ const COMPARE = {
87
+ greater: (a, b) => a > b,
88
+ "greater-or-equal": (a, b) => a >= b,
89
+ less: (a, b) => a < b,
90
+ "less-or-equal": (a, b) => a <= b,
91
+ equal: (a, b) => a === b,
92
+ "not-equal": (a, b) => a !== b,
93
+ };
94
+ function options(table) {
95
+ return Object.keys(table).map((value) => ({ value, label: label(value) }));
96
+ }
97
+ function label(value) {
98
+ return value
99
+ .split("-")
100
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
101
+ .join(" ");
102
+ }
103
+ export const binaryDefinition = {
104
+ apiVersion: 1,
105
+ label: "Binary Math",
106
+ description: "Two values, one operation. Component-wise, and a scalar broadcasts against a vector.",
107
+ icon: "Plus",
108
+ runsOn: "portable",
109
+ inputs: {
110
+ a: { kind: "data", type: "any", default: 0, description: "First value." },
111
+ b: { kind: "data", type: "any", default: 0, description: "Second value." },
112
+ },
113
+ outputs: { value: { kind: "data", type: "any" } },
114
+ props: {
115
+ operation: {
116
+ type: "string",
117
+ default: "add",
118
+ control: "select",
119
+ options: options(BINARY),
120
+ label: "Operation",
121
+ },
122
+ },
123
+ };
124
+ export function executeBinary(context) {
125
+ const operate = BINARY[context.props.operation];
126
+ if (!operate)
127
+ throw new Error(`cascade.math.Binary has no operation "${context.props.operation}"`);
128
+ context.outputs.value.set(combine(context.inputs.a, context.inputs.b, operate));
129
+ }
130
+ export const unaryDefinition = {
131
+ apiVersion: 1,
132
+ label: "Unary Math",
133
+ description: "One value, one operation. Trigonometry is in radians.",
134
+ icon: "FunctionSquare",
135
+ runsOn: "portable",
136
+ inputs: {
137
+ value: { kind: "data", type: "any", default: 0 },
138
+ },
139
+ outputs: { value: { kind: "data", type: "any" } },
140
+ props: {
141
+ operation: {
142
+ type: "string",
143
+ default: "abs",
144
+ control: "select",
145
+ options: options(UNARY),
146
+ label: "Operation",
147
+ },
148
+ },
149
+ };
150
+ export function executeUnary(context) {
151
+ const operate = UNARY[context.props.operation];
152
+ if (!operate)
153
+ throw new Error(`cascade.math.Unary has no operation "${context.props.operation}"`);
154
+ context.outputs.value.set(map(context.inputs.value, operate));
155
+ }
156
+ export const clampDefinition = {
157
+ apiVersion: 1,
158
+ label: "Clamp",
159
+ description: "Hold a value between two bounds.",
160
+ icon: "Brackets",
161
+ runsOn: "portable",
162
+ inputs: {
163
+ value: { kind: "data", type: "any", default: 0 },
164
+ min: { kind: "data", type: "any", default: 0 },
165
+ max: { kind: "data", type: "any", default: 1 },
166
+ },
167
+ outputs: { value: { kind: "data", type: "any" } },
168
+ props: {},
169
+ };
170
+ export function executeClamp(context) {
171
+ const lower = combine(context.inputs.value, context.inputs.min, Math.max);
172
+ context.outputs.value.set(combine(lower, context.inputs.max, Math.min));
173
+ }
174
+ export const mixDefinition = {
175
+ apiVersion: 1,
176
+ label: "Mix",
177
+ description: "Blend between two values. Houdini's lerp.",
178
+ icon: "Blend",
179
+ runsOn: "portable",
180
+ inputs: {
181
+ a: { kind: "data", type: "any", default: 0, description: "The value at 0." },
182
+ b: { kind: "data", type: "any", default: 1, description: "The value at 1." },
183
+ bias: {
184
+ kind: "data",
185
+ type: "float",
186
+ default: 0.5,
187
+ min: 0,
188
+ max: 1,
189
+ step: 0.01,
190
+ description: "Not clamped: past 0..1 this extrapolates, which is useful.",
191
+ },
192
+ },
193
+ outputs: { value: { kind: "data", type: "any" } },
194
+ props: {},
195
+ };
196
+ export function executeMix(context) {
197
+ const bias = context.inputs.bias ?? 0.5;
198
+ // a + (b - a) * bias, component-wise. Written this way rather than as
199
+ // `a*(1-t) + b*t` because it is exact at t = 0 and t = 1 in floating point,
200
+ // which matters when a mix drives something that must land exactly on an end.
201
+ const delta = combine(context.inputs.b, context.inputs.a, (b, a) => b - a);
202
+ context.outputs.value.set(combine(context.inputs.a, delta, (a, d) => a + d * bias));
203
+ }
204
+ export const compareDefinition = {
205
+ apiVersion: 1,
206
+ label: "Compare",
207
+ description: "Test two values. Outputs a bool, for a Switch.",
208
+ icon: "Equal",
209
+ runsOn: "portable",
210
+ inputs: {
211
+ a: { kind: "data", type: "float", default: 0 },
212
+ b: { kind: "data", type: "float", default: 0 },
213
+ },
214
+ outputs: { result: { kind: "data", type: "bool" } },
215
+ props: {
216
+ operation: {
217
+ type: "string",
218
+ default: "greater",
219
+ control: "select",
220
+ options: options(COMPARE),
221
+ label: "Test",
222
+ },
223
+ /**
224
+ * Scalars only, unlike the rest of this namespace.
225
+ *
226
+ * "Is this vector greater than that one" has no single right answer — all
227
+ * components, any component, or the length — and a node that picked one
228
+ * silently would be wrong for the other two readings. A comparison feeding
229
+ * a `Switch` wants one boolean, so the input is a `float` and the question
230
+ * does not arise.
231
+ */
232
+ tolerance: {
233
+ type: "float",
234
+ default: 0,
235
+ min: 0,
236
+ label: "Tolerance",
237
+ description: "Slack for Equal and Not Equal. Floating point rarely lands on exactly equal.",
238
+ },
239
+ },
240
+ };
241
+ export function executeCompare(context) {
242
+ const operation = context.props.operation;
243
+ const test = COMPARE[operation];
244
+ if (!test)
245
+ throw new Error(`cascade.math.Compare has no test "${context.props.operation}"`);
246
+ const a = context.inputs.a ?? 0;
247
+ const b = context.inputs.b ?? 0;
248
+ const tolerance = context.props.tolerance;
249
+ if (tolerance > 0 && (operation === "equal" || operation === "not-equal")) {
250
+ const near = Math.abs(a - b) <= tolerance;
251
+ context.outputs.result.set(operation === "equal" ? near : !near);
252
+ return;
253
+ }
254
+ context.outputs.result.set(test(a, b));
255
+ }
256
+ function registration(name, definition, execute) {
257
+ return {
258
+ kind: "definition-v1",
259
+ moduleId: `cascade.math.${name}`,
260
+ definition,
261
+ loadExecute: async () => execute,
262
+ };
263
+ }
264
+ export const binaryRegistration = registration("Binary", binaryDefinition, executeBinary);
265
+ export const unaryRegistration = registration("Unary", unaryDefinition, executeUnary);
266
+ export const clampRegistration = registration("Clamp", clampDefinition, executeClamp);
267
+ export const mixRegistration = registration("Mix", mixDefinition, executeMix);
268
+ export const compareRegistration = registration("Compare", compareDefinition, executeCompare);
269
+ export const mathNodeRegistrations = Object.freeze([
270
+ binaryRegistration,
271
+ unaryRegistration,
272
+ clampRegistration,
273
+ mixRegistration,
274
+ compareRegistration,
275
+ ]);
276
+ /** `[moduleId, definition]` pairs, the shape a host's palette builder wants. */
277
+ export const mathNodeDefinitions = Object.freeze([
278
+ ["cascade.math.Binary", binaryDefinition],
279
+ ["cascade.math.Unary", unaryDefinition],
280
+ ["cascade.math.Clamp", clampDefinition],
281
+ ["cascade.math.Mix", mixDefinition],
282
+ ["cascade.math.Compare", compareDefinition],
283
+ ]);
284
+ export { BINARY, UNARY, COMPARE };
285
+ //# sourceMappingURL=definitions.js.map
@@ -0,0 +1,2 @@
1
+ export * from "./definitions.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export * from "./definitions.js";
2
+ //# sourceMappingURL=index.js.map
@@ -17,6 +17,7 @@ export * from "./matrix.js";
17
17
  export * from "./merge.js";
18
18
  export * from "./primitives.js";
19
19
  export * from "./promote.js";
20
+ export * from "./rects.js";
20
21
  export * from "./resample.js";
21
22
  export * from "./svg.js";
22
23
  export * from "./transform.js";
@@ -17,6 +17,7 @@ export * from "./matrix.js";
17
17
  export * from "./merge.js";
18
18
  export * from "./primitives.js";
19
19
  export * from "./promote.js";
20
+ export * from "./rects.js";
20
21
  export * from "./resample.js";
21
22
  export * from "./svg.js";
22
23
  export * from "./transform.js";
@@ -0,0 +1,88 @@
1
+ import type { Geometry, Rect, Rects } from "@field/cascade/contracts";
2
+ /**
3
+ * `rects` into `geometry` — the conversion that gives the type consumers.
4
+ *
5
+ * `rects` has been a declared core type with **zero consumers in the runtime**:
6
+ * nothing converted it, `cascade.geo.SvgExport` takes only `geometry`, and
7
+ * `cloud-plots` rendered its rects through its own Python export instead.
8
+ * Marcus decided on 2026-09-12 that this lands now rather than after the
9
+ * cutover.
10
+ *
11
+ * ## Why a conversion and not a second input on `SvgExport`
12
+ *
13
+ * MW-OBSERVATORY-ART's argument, as the owner of the consuming code, and it is
14
+ * the right one: a second input would make **every** geometry consumer learn
15
+ * about `rects` separately — the SVG writer, then the renderer, then the
16
+ * viewport — which is three type vocabularies again by another route. One
17
+ * conversion means `rects` reaches all of them at once and none of them
18
+ * changes.
19
+ *
20
+ * ## Two wire shapes, because the producer and the contract disagree
21
+ *
22
+ * `packages/contracts` declares a rect as `{ x, y, width, height, tag? }`. The
23
+ * only thing that actually produces rects — `cloud-plots`' Python
24
+ * `node_cli.py` — emits `{ "rect": [x, y, w, h], "tag": "<name>" }`, measured
25
+ * by MW-OBSERVATORY-ART rather than remembered. Both are accepted here.
26
+ *
27
+ * Being permissive is deliberate: the declared shape has no producer and the
28
+ * produced shape has no declaration, so refusing either would mean the
29
+ * conversion works for nothing that exists or for nothing that is written
30
+ * down. Which of the two becomes canonical is a separate decision, and this
31
+ * keeps working whichever way it goes.
32
+ *
33
+ * ## The tag rides along, and that was the condition
34
+ *
35
+ * *"A conversion that keeps the four numbers and drops the tag would silently
36
+ * destroy the labelling, and the failure would show up as unlabelled marks in
37
+ * a poster rather than as an error."* So every rect's tag becomes a
38
+ * **primitive-level string attribute** named `tag` — geometry has string
39
+ * attributes, so this is lossless rather than an approximation, and `SvgExport`
40
+ * and the renderer already read primitive attributes for style.
41
+ */
42
+ /** The shape `cloud-plots` actually emits. */
43
+ interface TaggedBox {
44
+ readonly rect: readonly [number, number, number, number];
45
+ readonly tag?: string;
46
+ }
47
+ export type RectLike = Rect | TaggedBox;
48
+ export interface RectsToGeometryOptions {
49
+ /**
50
+ * Where a rect's `y` is measured from.
51
+ *
52
+ * **`top-left` is raster space**, which is what `cloud-plots` produces —
53
+ * pixel coordinates against the image, clamped to its bounds by
54
+ * `_clamp_rects`. Cascade geometry is **+Y up**, and `geometryToSvg` flips on
55
+ * the way out, so a raster rect passed through unflipped comes out of the SVG
56
+ * writer mirrored. Converting with `top-left` and a `height` is what makes
57
+ * the picture come back the way the analyst saw it.
58
+ *
59
+ * `bottom-left` passes the numbers through and is right for anything already
60
+ * in geometry's own space.
61
+ */
62
+ readonly origin?: "top-left" | "bottom-left";
63
+ /**
64
+ * The image height, for `top-left`. Zero means no flip.
65
+ *
66
+ * Required rather than inferred, because the rects themselves do not carry
67
+ * the frame they were measured against — flipping about the tallest rect
68
+ * would be a guess that looks right on a full-height mark and wrong on every
69
+ * other one.
70
+ */
71
+ readonly height?: number;
72
+ }
73
+ /**
74
+ * One closed four-point polygon per rect, counter-clockwise from the corner
75
+ * nearest the origin — the same winding `rectangleGeometry` produces, so a
76
+ * converted rect and an authored rectangle behave identically downstream.
77
+ *
78
+ * A rect with a non-finite or missing number is **skipped**, and the count of
79
+ * what was skipped is on the geometry as a `rects_skipped` detail. Throwing
80
+ * would make one bad mark in a thousand lose the other nine hundred and
81
+ * ninety-nine; silence would lose them without saying so.
82
+ */
83
+ export declare function rectsToGeometry(value: Rects | readonly RectLike[] | null | undefined, options?: RectsToGeometryOptions): Geometry;
84
+ /** True for either accepted wire shape. Used by the node to say what arrived
85
+ * when something else does. */
86
+ export declare function isRectLike(value: unknown): value is RectLike;
87
+ export {};
88
+ //# sourceMappingURL=rects.d.ts.map
@@ -0,0 +1,68 @@
1
+ import { GeometryBuilder } from "./builder.js";
2
+ function coordinates(rect) {
3
+ if (Array.isArray(rect.rect)) {
4
+ const box = rect.rect;
5
+ if (box.length < 4 || box.some((value) => typeof value !== "number"))
6
+ return null;
7
+ return [box[0], box[1], box[2], box[3]];
8
+ }
9
+ const declared = rect;
10
+ if (typeof declared?.x !== "number" ||
11
+ typeof declared?.y !== "number" ||
12
+ typeof declared?.width !== "number" ||
13
+ typeof declared?.height !== "number")
14
+ return null;
15
+ return [declared.x, declared.y, declared.width, declared.height];
16
+ }
17
+ /**
18
+ * One closed four-point polygon per rect, counter-clockwise from the corner
19
+ * nearest the origin — the same winding `rectangleGeometry` produces, so a
20
+ * converted rect and an authored rectangle behave identically downstream.
21
+ *
22
+ * A rect with a non-finite or missing number is **skipped**, and the count of
23
+ * what was skipped is on the geometry as a `rects_skipped` detail. Throwing
24
+ * would make one bad mark in a thousand lose the other nine hundred and
25
+ * ninety-nine; silence would lose them without saying so.
26
+ */
27
+ export function rectsToGeometry(value, options = {}) {
28
+ const list = Array.isArray(value)
29
+ ? value
30
+ : (value?.rects ?? []);
31
+ const flip = options.origin === "top-left" ? (options.height ?? 0) : 0;
32
+ const builder = new GeometryBuilder();
33
+ const tags = [];
34
+ let skipped = 0;
35
+ let tagged = false;
36
+ for (const rect of list) {
37
+ const box = coordinates(rect);
38
+ if (!box || box.some((component) => !Number.isFinite(component))) {
39
+ skipped += 1;
40
+ continue;
41
+ }
42
+ const [x, y, width, height] = box;
43
+ // In raster space `y` is the TOP edge and grows downwards, so the flip is
44
+ // about the frame and the box's own height comes back off it.
45
+ const bottom = flip > 0 ? flip - (y + height) : y;
46
+ const top = bottom + height;
47
+ builder.addPolygon([x, bottom, x + width, bottom, x + width, top, x, top], {
48
+ closed: true,
49
+ });
50
+ const tag = rect.tag;
51
+ tags.push(typeof tag === "string" ? tag : "");
52
+ if (typeof tag === "string" && tag.length > 0)
53
+ tagged = true;
54
+ }
55
+ // Only when something is actually labelled: an attribute of empty strings on
56
+ // every primitive is noise in the Inspector and in every exported file.
57
+ if (tagged)
58
+ builder.setStringAttribute("primitive", "tag", tags);
59
+ if (skipped > 0)
60
+ builder.setDetail("rects_skipped", skipped);
61
+ return builder.build();
62
+ }
63
+ /** True for either accepted wire shape. Used by the node to say what arrived
64
+ * when something else does. */
65
+ export function isRectLike(value) {
66
+ return (typeof value === "object" && value !== null && coordinates(value) !== null);
67
+ }
68
+ //# sourceMappingURL=rects.js.map
@@ -369,6 +369,31 @@ Runs on `portable`.
369
369
  | --- | --- | --- | --- |
370
370
  | `geometry` | `geometry` | | |
371
371
 
372
+ ### `cascade.geo.FromRects`
373
+
374
+ From Rects — Convert rects into geometry, one closed polygon each, keeping the tag.
375
+
376
+ Runs on `portable`.
377
+
378
+ **Inputs**
379
+
380
+ | name | type | default | range |
381
+ | --- | --- | --- | --- |
382
+ | `rects` | `rects` | | |
383
+ | `height` | `float` | `0` | |
384
+
385
+ **Props**
386
+
387
+ | name | type | default | range |
388
+ | --- | --- | --- | --- |
389
+ | `origin` | `string` | `"top-left"` | |
390
+
391
+ **Outputs**
392
+
393
+ | name | type | default | range |
394
+ | --- | --- | --- | --- |
395
+ | `geometry` | `geometry` | | |
396
+
372
397
  ### `cascade.geo.Merge`
373
398
 
374
399
  Merge — Concatenate geometries, taking the union of their attributes.
@@ -498,6 +523,121 @@ Runs on `portable`.
498
523
  | --- | --- | --- | --- |
499
524
  | `geometry` | `geometry` | | |
500
525
 
526
+ ### `cascade.math.Binary`
527
+
528
+ Binary Math — Two values, one operation. Component-wise, and a scalar broadcasts against a vector.
529
+
530
+ Runs on `portable`.
531
+
532
+ **Inputs**
533
+
534
+ | name | type | default | range |
535
+ | --- | --- | --- | --- |
536
+ | `a` | `any` | `0` | |
537
+ | `b` | `any` | `0` | |
538
+
539
+ **Props**
540
+
541
+ | name | type | default | range |
542
+ | --- | --- | --- | --- |
543
+ | `operation` | `string` | `"add"` | |
544
+
545
+ **Outputs**
546
+
547
+ | name | type | default | range |
548
+ | --- | --- | --- | --- |
549
+ | `value` | `any` | | |
550
+
551
+ ### `cascade.math.Clamp`
552
+
553
+ Clamp — Hold a value between two bounds.
554
+
555
+ Runs on `portable`.
556
+
557
+ **Inputs**
558
+
559
+ | name | type | default | range |
560
+ | --- | --- | --- | --- |
561
+ | `value` | `any` | `0` | |
562
+ | `min` | `any` | `0` | |
563
+ | `max` | `any` | `1` | |
564
+
565
+ **Outputs**
566
+
567
+ | name | type | default | range |
568
+ | --- | --- | --- | --- |
569
+ | `value` | `any` | | |
570
+
571
+ ### `cascade.math.Compare`
572
+
573
+ Compare — Test two values. Outputs a bool, for a Switch.
574
+
575
+ Runs on `portable`.
576
+
577
+ **Inputs**
578
+
579
+ | name | type | default | range |
580
+ | --- | --- | --- | --- |
581
+ | `a` | `float` | `0` | |
582
+ | `b` | `float` | `0` | |
583
+
584
+ **Props**
585
+
586
+ | name | type | default | range |
587
+ | --- | --- | --- | --- |
588
+ | `operation` | `string` | `"greater"` | |
589
+ | `tolerance` | `float` | `0` | |
590
+
591
+ **Outputs**
592
+
593
+ | name | type | default | range |
594
+ | --- | --- | --- | --- |
595
+ | `result` | `bool` | | |
596
+
597
+ ### `cascade.math.Mix`
598
+
599
+ Mix — Blend between two values. Houdini's lerp.
600
+
601
+ Runs on `portable`.
602
+
603
+ **Inputs**
604
+
605
+ | name | type | default | range |
606
+ | --- | --- | --- | --- |
607
+ | `a` | `any` | `0` | |
608
+ | `b` | `any` | `1` | |
609
+ | `bias` | `float` | `0.5` | 0…1 |
610
+
611
+ **Outputs**
612
+
613
+ | name | type | default | range |
614
+ | --- | --- | --- | --- |
615
+ | `value` | `any` | | |
616
+
617
+ ### `cascade.math.Unary`
618
+
619
+ Unary Math — One value, one operation. Trigonometry is in radians.
620
+
621
+ Runs on `portable`.
622
+
623
+ **Inputs**
624
+
625
+ | name | type | default | range |
626
+ | --- | --- | --- | --- |
627
+ | `value` | `any` | `0` | |
628
+
629
+ **Props**
630
+
631
+ | name | type | default | range |
632
+ | --- | --- | --- | --- |
633
+ | `operation` | `string` | `"abs"` | |
634
+
635
+ **Outputs**
636
+
637
+ | name | type | default | range |
638
+ | --- | --- | --- | --- |
639
+ | `value` | `any` | | |
640
+
501
641
  ### `cascade.pop.FieldForce`
502
642
 
503
643
  POP Field Force — Pull particles along a field's gradient and around its contours. The calligraphy force.
@@ -784,7 +924,7 @@ under `src/nodes/<library>/nodes/`.
784
924
 
785
925
  ### Geometry — `cascade.geo.*`
786
926
 
787
- **Geometry.** `cascade.geo.Rectangle` (A closed four-point polygon, counter-clockwise from bottom-left.), `cascade.geo.Circle` (A closed circle, as one cubic Bezier chain or as a polygon of divisions segments.), `cascade.geo.Transform` (Translate, rotate and scale geometry about a pivot.), `cascade.geo.Merge` (Concatenate geometries, taking the union of their attributes.), `cascade.geo.CopyToPoints` (Instance one geometry onto every point of another.), `cascade.geo.SvgExport` (Write geometry as an SVG document, one group element per group.), `cascade.geo.Render` (Draw a scene through a camera, as a raster.)
927
+ **Geometry.** `cascade.geo.Rectangle` (A closed four-point polygon, counter-clockwise from bottom-left.), `cascade.geo.Circle` (A closed circle, as one cubic Bezier chain or as a polygon of divisions segments.), `cascade.geo.Transform` (Translate, rotate and scale geometry about a pivot.), `cascade.geo.Merge` (Concatenate geometries, taking the union of their attributes.), `cascade.geo.CopyToPoints` (Instance one geometry onto every point of another.), `cascade.geo.SvgExport` (Write geometry as an SVG document, one group element per group.), `cascade.geo.Render` (Draw a scene through a camera, as a raster.), `cascade.geo.FromRects` (Convert rects into geometry, one closed polygon each, keeping the tag.)
788
928
 
789
929
  ### Image — `cascade.image.*`
790
930
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@field/cascade",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "description": "Visual programming for generative art",
5
5
  "license": "MIT",
6
6
  "author": "FIELD.IO <marcus@field.io>",
@@ -1 +0,0 @@
1
- import{D as e,c as t,l as n,p as r,u as i}from"./index-CeqTfMbr.js";e([r,t,n,i]);
@@ -1 +0,0 @@
1
- import{D as e,E as t,s as n,v as r}from"./index-CeqTfMbr.js";e(n,{assets:t});export{r as geoLibrary};
@@ -1 +0,0 @@
1
- import{D as e,_ as t,g as n}from"./index-CeqTfMbr.js";e(t,{});export{n as popLibrary};