@field/cascade 0.6.0 → 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 (48) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/assets/{CodeEditor-BLpahZw6.js → CodeEditor-DkpxJFRs.js} +4 -4
  3. package/dist/assets/{CodePanel-BqYKqBMH.js → CodePanel-O31InnQg.js} +2 -2
  4. package/dist/assets/{CustomNodeDialog-CYr9y7dG.js → CustomNodeDialog-Cn3sT-M2.js} +1 -1
  5. package/dist/assets/{ProjectSettings-CnYfrkKh.js → ProjectSettings-BzEmtsYc.js} +1 -1
  6. package/dist/assets/{SettingsDialog-mSS6KUyS.js → SettingsDialog-zWct7T8j.js} +1 -1
  7. package/dist/assets/{VersionHistoryDialog-DZuwNG6s.js → VersionHistoryDialog-DCe6XQcO.js} +1 -1
  8. package/dist/assets/{core-BOGrwBxU.js → core-DwPLRUzm.js} +1 -1
  9. package/dist/assets/{cssMode-0Z3pyDOW.js → cssMode-C1bjAPYj.js} +1 -1
  10. package/dist/assets/definitions-Dh-oZAA5.js +1 -0
  11. package/dist/assets/{editor.api-D-_7pnnj.js → editor.api-wr5lElMs.js} +1 -1
  12. package/dist/assets/{freemarker2-DE0JRkF7.js → freemarker2-DQHTaSDb.js} +1 -1
  13. package/dist/assets/geo-DLRNQg57.js +1 -0
  14. package/dist/assets/{handlebars-B_Shirzj.js → handlebars-DOiZYGbp.js} +1 -1
  15. package/dist/assets/{html-DI5Qj4gd.js → html-B1u4CWMY.js} +1 -1
  16. package/dist/assets/{htmlMode-CkHZy0mE.js → htmlMode-YNKJ8Hq1.js} +1 -1
  17. package/dist/assets/{image-B6RU2frM.js → image-DmeeS7Tl.js} +1 -1
  18. package/dist/assets/{index-DTyzl6YV.js → index-C35beBgj.js} +15 -15
  19. package/dist/assets/{javascript-zxQfbzS_.js → javascript-DVujl3OI.js} +1 -1
  20. package/dist/assets/{jsonMode-DSQ0uLqA.js → jsonMode-Bp_Ujoae.js} +1 -1
  21. package/dist/assets/{liquid-0zs2oOuA.js → liquid-C0wcDX3X.js} +1 -1
  22. package/dist/assets/{lspLanguageFeatures-7VWaq5lc.js → lspLanguageFeatures-C2XtKL_c.js} +1 -1
  23. package/dist/assets/math-D0hLbnR2.js +1 -0
  24. package/dist/assets/{mdx-BFKlF5Xh.js → mdx-Dz4Hb1ZN.js} +1 -1
  25. package/dist/assets/pop-OaOoEUwk.js +1 -0
  26. package/dist/assets/{python-BgexpWZh.js → python-Y4eTKlAp.js} +1 -1
  27. package/dist/assets/{razor-E1kWyvkT.js → razor-BmpH_QN_.js} +1 -1
  28. package/dist/assets/{tsMode-CTmJLLW5.js → tsMode-mOQwydYU.js} +1 -1
  29. package/dist/assets/{typescript-BheZLRDM.js → typescript-8ItD7azm.js} +1 -1
  30. package/dist/assets/{workers-KIoho05H.js → workers-xhda_-L2.js} +1 -1
  31. package/dist/assets/{xml-DaVLg9zI.js → xml-ydyZSquM.js} +1 -1
  32. package/dist/assets/{yaml-D6XGTfOj.js → yaml-BQjpTmW5.js} +1 -1
  33. package/dist/cli/index.js +3245 -11426
  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/geometry/index.d.ts +1 -0
  40. package/dist/runtime/geometry/index.js +1 -0
  41. package/dist/runtime/geometry/rects.d.ts +88 -0
  42. package/dist/runtime/geometry/rects.js +68 -0
  43. package/doc/NODE_REFERENCE.md +26 -1
  44. package/package.json +1 -1
  45. package/dist/assets/definitions-Cckh442l.js +0 -1
  46. package/dist/assets/geo-DcAVygkw.js +0 -1
  47. package/dist/assets/math-BA6juJD7.js +0 -1
  48. package/dist/assets/pop-Bkzzmp57.js +0 -1
package/dist/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Cascade</title>
7
- <script type="module" crossorigin src="/assets/index-DTyzl6YV.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-C35beBgj.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="/assets/legacy-CAj2_7QE.js">
9
9
  <link rel="modulepreload" crossorigin href="/assets/index-client-BoJgsN9M.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/nodeTypeUtils-BDJsc2Ks.js">
@@ -1,3 +1,4 @@
1
+ import type { NodeExecutionContext } from "@field/cascade/contracts";
1
2
  export declare const rectangleDefinition: {
2
3
  readonly apiVersion: 1;
3
4
  readonly label: "Rectangle";
@@ -387,6 +388,116 @@ export declare const renderDefinition: {
387
388
  };
388
389
  };
389
390
  };
391
+ /**
392
+ * `cascade.geo.FromRects` — the conversion, as a node.
393
+ *
394
+ * Named for what it does rather than for Houdini, which has no equivalent:
395
+ * `Rects` would read as a node that *produces* them, and `Convert` would be a
396
+ * general node with one mode in it.
397
+ *
398
+ * The whole point is that this is the only place `rects` is understood.
399
+ * `SvgExport`, `Render` and the viewport keep taking `geometry` and learn
400
+ * nothing — which is the argument MW-OBSERVATORY-ART made as owner of the
401
+ * consuming code, against adding a second input to the exporter.
402
+ *
403
+ * There is deliberately **no implicit `rects` → `geometry` conversion** in the
404
+ * type table. It would let a rects output wire straight into a geometry input,
405
+ * and the flip below would then happen invisibly or not at all — a picture
406
+ * silently mirrored is exactly the failure this node's `origin` prop exists to
407
+ * make a decision.
408
+ */
409
+ export declare const fromRectsDefinition: {
410
+ readonly apiVersion: 1;
411
+ readonly label: "From Rects";
412
+ readonly description: "Convert rects into geometry, one closed polygon each, keeping the tag.";
413
+ readonly icon: "Frame";
414
+ readonly runsOn: "portable";
415
+ readonly inputs: {
416
+ readonly rects: {
417
+ readonly kind: "data";
418
+ readonly type: "rects";
419
+ };
420
+ /** The frame the rects were measured against, for the flip. */
421
+ readonly height: {
422
+ readonly kind: "data";
423
+ readonly type: "float";
424
+ readonly default: 0;
425
+ readonly min: 0;
426
+ readonly description: "Image height, for Top Left. Zero means no flip.";
427
+ };
428
+ };
429
+ readonly outputs: {
430
+ readonly geometry: {
431
+ readonly kind: "data";
432
+ readonly type: "geometry";
433
+ };
434
+ };
435
+ readonly props: {
436
+ readonly origin: {
437
+ readonly type: "string";
438
+ readonly default: "top-left";
439
+ readonly control: "select";
440
+ readonly options: readonly [{
441
+ readonly value: "top-left";
442
+ readonly label: "Top Left - raster, needs a height";
443
+ }, {
444
+ readonly value: "bottom-left";
445
+ readonly label: "Bottom Left - already +Y up";
446
+ }];
447
+ readonly label: "Origin";
448
+ readonly description: "Where y is measured from. Raster rects need flipping or the SVG comes out mirrored.";
449
+ };
450
+ };
451
+ };
452
+ export declare function executeFromRects(context: NodeExecutionContext<typeof fromRectsDefinition>): void;
453
+ export declare const fromRectsRegistration: {
454
+ kind: "definition-v1";
455
+ moduleId: string;
456
+ definition: {
457
+ readonly apiVersion: 1;
458
+ readonly label: "From Rects";
459
+ readonly description: "Convert rects into geometry, one closed polygon each, keeping the tag.";
460
+ readonly icon: "Frame";
461
+ readonly runsOn: "portable";
462
+ readonly inputs: {
463
+ readonly rects: {
464
+ readonly kind: "data";
465
+ readonly type: "rects";
466
+ };
467
+ /** The frame the rects were measured against, for the flip. */
468
+ readonly height: {
469
+ readonly kind: "data";
470
+ readonly type: "float";
471
+ readonly default: 0;
472
+ readonly min: 0;
473
+ readonly description: "Image height, for Top Left. Zero means no flip.";
474
+ };
475
+ };
476
+ readonly outputs: {
477
+ readonly geometry: {
478
+ readonly kind: "data";
479
+ readonly type: "geometry";
480
+ };
481
+ };
482
+ readonly props: {
483
+ readonly origin: {
484
+ readonly type: "string";
485
+ readonly default: "top-left";
486
+ readonly control: "select";
487
+ readonly options: readonly [{
488
+ readonly value: "top-left";
489
+ readonly label: "Top Left - raster, needs a height";
490
+ }, {
491
+ readonly value: "bottom-left";
492
+ readonly label: "Bottom Left - already +Y up";
493
+ }];
494
+ readonly label: "Origin";
495
+ readonly description: "Where y is measured from. Raster rects need flipping or the SVG comes out mirrored.";
496
+ };
497
+ };
498
+ };
499
+ loadExecute: () => Promise<typeof executeFromRects>;
500
+ };
390
501
  export declare const geoNodeDefinitions: readonly [readonly ["cascade.geo.Rectangle", {
391
502
  readonly apiVersion: 1;
392
503
  readonly label: "Rectangle";
@@ -769,5 +880,47 @@ export declare const geoNodeDefinitions: readonly [readonly ["cascade.geo.Rectan
769
880
  readonly step: 0.1;
770
881
  };
771
882
  };
883
+ }], readonly ["cascade.geo.FromRects", {
884
+ readonly apiVersion: 1;
885
+ readonly label: "From Rects";
886
+ readonly description: "Convert rects into geometry, one closed polygon each, keeping the tag.";
887
+ readonly icon: "Frame";
888
+ readonly runsOn: "portable";
889
+ readonly inputs: {
890
+ readonly rects: {
891
+ readonly kind: "data";
892
+ readonly type: "rects";
893
+ };
894
+ /** The frame the rects were measured against, for the flip. */
895
+ readonly height: {
896
+ readonly kind: "data";
897
+ readonly type: "float";
898
+ readonly default: 0;
899
+ readonly min: 0;
900
+ readonly description: "Image height, for Top Left. Zero means no flip.";
901
+ };
902
+ };
903
+ readonly outputs: {
904
+ readonly geometry: {
905
+ readonly kind: "data";
906
+ readonly type: "geometry";
907
+ };
908
+ };
909
+ readonly props: {
910
+ readonly origin: {
911
+ readonly type: "string";
912
+ readonly default: "top-left";
913
+ readonly control: "select";
914
+ readonly options: readonly [{
915
+ readonly value: "top-left";
916
+ readonly label: "Top Left - raster, needs a height";
917
+ }, {
918
+ readonly value: "bottom-left";
919
+ readonly label: "Bottom Left - already +Y up";
920
+ }];
921
+ readonly label: "Origin";
922
+ readonly description: "Where y is measured from. Raster rects need flipping or the SVG comes out mirrored.";
923
+ };
924
+ };
772
925
  }]];
773
926
  //# sourceMappingURL=definitions.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { rectsToGeometry } from "../../geometry/rects.js";
1
2
  export const rectangleDefinition = {
2
3
  apiVersion: 1,
3
4
  label: "Rectangle",
@@ -240,6 +241,79 @@ export const renderDefinition = {
240
241
  pointRadius: { type: "float", default: 1.5, min: 0, max: 50, step: 0.1 },
241
242
  },
242
243
  };
244
+ /**
245
+ * `cascade.geo.FromRects` — the conversion, as a node.
246
+ *
247
+ * Named for what it does rather than for Houdini, which has no equivalent:
248
+ * `Rects` would read as a node that *produces* them, and `Convert` would be a
249
+ * general node with one mode in it.
250
+ *
251
+ * The whole point is that this is the only place `rects` is understood.
252
+ * `SvgExport`, `Render` and the viewport keep taking `geometry` and learn
253
+ * nothing — which is the argument MW-OBSERVATORY-ART made as owner of the
254
+ * consuming code, against adding a second input to the exporter.
255
+ *
256
+ * There is deliberately **no implicit `rects` → `geometry` conversion** in the
257
+ * type table. It would let a rects output wire straight into a geometry input,
258
+ * and the flip below would then happen invisibly or not at all — a picture
259
+ * silently mirrored is exactly the failure this node's `origin` prop exists to
260
+ * make a decision.
261
+ */
262
+ export const fromRectsDefinition = {
263
+ apiVersion: 1,
264
+ label: "From Rects",
265
+ description: "Convert rects into geometry, one closed polygon each, keeping the tag.",
266
+ icon: "Frame",
267
+ runsOn: "portable",
268
+ inputs: {
269
+ rects: { kind: "data", type: "rects" },
270
+ /** The frame the rects were measured against, for the flip. */
271
+ height: {
272
+ kind: "data",
273
+ type: "float",
274
+ default: 0,
275
+ min: 0,
276
+ description: "Image height, for Top Left. Zero means no flip.",
277
+ },
278
+ },
279
+ outputs: { geometry: { kind: "data", type: "geometry" } },
280
+ props: {
281
+ origin: {
282
+ type: "string",
283
+ default: "top-left",
284
+ control: "select",
285
+ options: [
286
+ { value: "top-left", label: "Top Left - raster, needs a height" },
287
+ { value: "bottom-left", label: "Bottom Left - already +Y up" },
288
+ ],
289
+ label: "Origin",
290
+ description: "Where y is measured from. Raster rects need flipping or the SVG comes out mirrored.",
291
+ },
292
+ },
293
+ };
294
+ export function executeFromRects(context) {
295
+ const incoming = context.inputs.rects;
296
+ const origin = context.props.origin === "bottom-left" ? "bottom-left" : "top-left";
297
+ const height = context.inputs.height ?? 0;
298
+ // A raster conversion with no height is the one mistake worth refusing: it
299
+ // produces a mirrored picture that looks plausible, and a silently upside
300
+ // down poster is harder to notice than a red node.
301
+ if (origin === "top-left" && height <= 0 && rectCount(incoming) > 0)
302
+ throw new Error("cascade.geo.FromRects needs the image height to flip raster rects — wire it, or set Origin to Bottom Left if the rects are already +Y up");
303
+ context.outputs.geometry.set(rectsToGeometry(incoming, { origin, height }));
304
+ }
305
+ function rectCount(value) {
306
+ if (Array.isArray(value))
307
+ return value.length;
308
+ const rects = value?.rects;
309
+ return Array.isArray(rects) ? rects.length : 0;
310
+ }
311
+ export const fromRectsRegistration = {
312
+ kind: "definition-v1",
313
+ moduleId: "cascade.geo.FromRects",
314
+ definition: fromRectsDefinition,
315
+ loadExecute: async () => executeFromRects,
316
+ };
243
317
  export const geoNodeDefinitions = Object.freeze([
244
318
  ["cascade.geo.Rectangle", rectangleDefinition],
245
319
  ["cascade.geo.Circle", circleDefinition],
@@ -248,5 +322,6 @@ export const geoNodeDefinitions = Object.freeze([
248
322
  ["cascade.geo.CopyToPoints", copyToPointsDefinition],
249
323
  ["cascade.geo.SvgExport", svgExportDefinition],
250
324
  ["cascade.geo.Render", renderDefinition],
325
+ ["cascade.geo.FromRects", fromRectsDefinition],
251
326
  ]);
252
327
  //# sourceMappingURL=definitions.js.map
@@ -1,5 +1,7 @@
1
1
  import type { DefinitionNodeRegistration } from "../../types.js";
2
2
  import { copyToPointsRegistration } from "./copyToPoints.js";
3
+ import { fromRectsRegistration } from "./definitions.js";
4
+ export { executeFromRects, fromRectsDefinition } from "./definitions.js";
3
5
  import { circleRegistration, rectangleRegistration } from "./generators.js";
4
6
  import { mergeRegistration } from "./merge.js";
5
7
  import { renderRegistration } from "./render.js";
@@ -17,7 +19,7 @@ import { transformRegistration } from "./transform.js";
17
19
  *
18
20
  * The namespace lives in `namespace.ts`, once.
19
21
  */
20
- export { circleRegistration, copyToPointsRegistration, mergeRegistration, rectangleRegistration, renderRegistration, svgExportRegistration, transformRegistration, };
22
+ export { circleRegistration, fromRectsRegistration, copyToPointsRegistration, mergeRegistration, rectangleRegistration, renderRegistration, svgExportRegistration, transformRegistration, };
21
23
  export { GEO_NAMESPACE, geoModuleId } from "./namespace.js";
22
24
  export declare const geoNodeRegistrations: readonly DefinitionNodeRegistration[];
23
25
  export declare function geoNodeRegistration(moduleId: string): DefinitionNodeRegistration | undefined;
@@ -1,4 +1,6 @@
1
1
  import { copyToPointsRegistration } from "./copyToPoints.js";
2
+ import { fromRectsRegistration } from "./definitions.js";
3
+ export { executeFromRects, fromRectsDefinition } from "./definitions.js";
2
4
  import { circleRegistration, rectangleRegistration } from "./generators.js";
3
5
  import { mergeRegistration } from "./merge.js";
4
6
  import { renderRegistration } from "./render.js";
@@ -16,7 +18,7 @@ import { transformRegistration } from "./transform.js";
16
18
  *
17
19
  * The namespace lives in `namespace.ts`, once.
18
20
  */
19
- export { circleRegistration, copyToPointsRegistration, mergeRegistration, rectangleRegistration, renderRegistration, svgExportRegistration, transformRegistration, };
21
+ export { circleRegistration, fromRectsRegistration, copyToPointsRegistration, mergeRegistration, rectangleRegistration, renderRegistration, svgExportRegistration, transformRegistration, };
20
22
  export { GEO_NAMESPACE, geoModuleId } from "./namespace.js";
21
23
  export const geoNodeRegistrations = Object.freeze([
22
24
  rectangleRegistration,
@@ -26,6 +28,7 @@ export const geoNodeRegistrations = Object.freeze([
26
28
  copyToPointsRegistration,
27
29
  renderRegistration,
28
30
  svgExportRegistration,
31
+ fromRectsRegistration,
29
32
  ]);
30
33
  const registrationsById = new Map(geoNodeRegistrations.map((registration) => [
31
34
  registration.moduleId,
@@ -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.
@@ -899,7 +924,7 @@ under `src/nodes/<library>/nodes/`.
899
924
 
900
925
  ### Geometry — `cascade.geo.*`
901
926
 
902
- **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.)
903
928
 
904
929
  ### Image — `cascade.image.*`
905
930
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@field/cascade",
3
- "version": "0.6.0",
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{O as e,d as t,l as n,m as r,u as i}from"./index-DTyzl6YV.js";e([r,n,i,t]);
@@ -1 +0,0 @@
1
- import{D as e,O as t,c as n,y as r}from"./index-DTyzl6YV.js";t(n,{assets:e});export{r as geoLibrary};
@@ -1 +0,0 @@
1
- import{O as e,s as t}from"./index-DTyzl6YV.js";e(t,{});
@@ -1 +0,0 @@
1
- import{O as e,_ as t,v as n}from"./index-DTyzl6YV.js";e(n,{});export{t as popLibrary};