@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.
- package/CHANGELOG.md +91 -0
- package/dist/assets/{CodeEditor-BHej8kM9.js → CodeEditor-DkpxJFRs.js} +7 -7
- package/dist/assets/{CodePanel-CT3eirK8.js → CodePanel-O31InnQg.js} +2 -2
- package/dist/assets/{CustomNodeDialog-BG0m7_Rv.js → CustomNodeDialog-Cn3sT-M2.js} +1 -1
- package/dist/assets/{ProjectSettings-BxoLWJG6.js → ProjectSettings-BzEmtsYc.js} +2 -2
- package/dist/assets/{SettingsDialog-r9PzZ22z.js → SettingsDialog-zWct7T8j.js} +1 -1
- package/dist/assets/{VersionHistoryDialog-B6zUV6Dy.js → VersionHistoryDialog-DCe6XQcO.js} +1 -1
- package/dist/assets/{core-wwx5881P.js → core-DwPLRUzm.js} +2 -2
- package/dist/assets/{cssMode-DY5T2Or0.js → cssMode-C1bjAPYj.js} +1 -1
- package/dist/assets/definitions-Dh-oZAA5.js +1 -0
- package/dist/assets/{editor.api-mkORHEMm.js → editor.api-wr5lElMs.js} +1 -1
- package/dist/assets/{freemarker2-CIWlk0sw.js → freemarker2-DQHTaSDb.js} +1 -1
- package/dist/assets/geo-DLRNQg57.js +1 -0
- package/dist/assets/{handlebars-t4jbj3Br.js → handlebars-DOiZYGbp.js} +1 -1
- package/dist/assets/{html-Ctx7UOlY.js → html-B1u4CWMY.js} +1 -1
- package/dist/assets/{htmlMode-BKNZxVw7.js → htmlMode-YNKJ8Hq1.js} +1 -1
- package/dist/assets/{image-BCBFz0gD.js → image-DmeeS7Tl.js} +3 -3
- package/dist/assets/{index-CeqTfMbr.js → index-C35beBgj.js} +36 -36
- package/dist/assets/{javascript-Da_TJBzH.js → javascript-DVujl3OI.js} +1 -1
- package/dist/assets/{jsonMode-Ftt5d1nV.js → jsonMode-Bp_Ujoae.js} +1 -1
- package/dist/assets/{liquid-D5kWYG57.js → liquid-C0wcDX3X.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-B-93LC_L.js → lspLanguageFeatures-C2XtKL_c.js} +1 -1
- package/dist/assets/math-D0hLbnR2.js +1 -0
- package/dist/assets/{mdx-DGT3C_qs.js → mdx-Dz4Hb1ZN.js} +1 -1
- package/dist/assets/pop-OaOoEUwk.js +1 -0
- package/dist/assets/{python-Bz3Eew5S.js → python-Y4eTKlAp.js} +1 -1
- package/dist/assets/{razor-CFDdKpKI.js → razor-BmpH_QN_.js} +1 -1
- package/dist/assets/{tsMode-BaCINWBI.js → tsMode-mOQwydYU.js} +1 -1
- package/dist/assets/{typescript-BoOWspVs.js → typescript-8ItD7azm.js} +1 -1
- package/dist/assets/{workers-dSWyDxsQ.js → workers-xhda_-L2.js} +1 -1
- package/dist/assets/{xml-eFi1_vYt.js → xml-ydyZSquM.js} +1 -1
- package/dist/assets/{yaml-a7xrJHxm.js → yaml-BQjpTmW5.js} +1 -1
- package/dist/cli/index.js +11259 -19155
- package/dist/index.html +1 -1
- package/dist/runtime/builtins/geo/definitions.d.ts +153 -0
- package/dist/runtime/builtins/geo/definitions.js +75 -0
- package/dist/runtime/builtins/geo/index.d.ts +3 -1
- package/dist/runtime/builtins/geo/index.js +4 -1
- package/dist/runtime/builtins/index.js +2 -0
- package/dist/runtime/builtins/math/definitions.d.ts +845 -0
- package/dist/runtime/builtins/math/definitions.js +285 -0
- package/dist/runtime/builtins/math/index.d.ts +2 -0
- package/dist/runtime/builtins/math/index.js +2 -0
- package/dist/runtime/geometry/index.d.ts +1 -0
- package/dist/runtime/geometry/index.js +1 -0
- package/dist/runtime/geometry/rects.d.ts +88 -0
- package/dist/runtime/geometry/rects.js +68 -0
- package/doc/NODE_REFERENCE.md +141 -1
- package/package.json +1 -1
- package/dist/assets/definitions-BgCvRHyR.js +0 -1
- package/dist/assets/geo-CIxgs0oE.js +0 -1
- 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,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
|
package/doc/NODE_REFERENCE.md
CHANGED
|
@@ -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 +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};
|