@vibecook/ice 0.9.0 → 0.11.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.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The MAGNET grid renderer — TSL port of vibe-field draft/magnet-grid's
3
+ * `magnet.wgsl` (design-010 §5). Three instanced-quad meshes (one per lattice
4
+ * level) whose vertex stage reconstructs sites from `instanceIndex`, evaluates
5
+ * the field (superposition of rounded-box SDF sources; poles are DEGENERATE
6
+ * boxes with half = 0, r = 0 — D3), and shapes the glyph (needle orients
7
+ * along the field; dot swells with |field|).
8
+ *
9
+ * Deltas from the WGSL, per design-010:
10
+ * - ONE source buffer (storage, read-only, `setPBO` for the WebGL fallback —
11
+ * D4); the mouse uniform / `mouseOn` branch are deleted.
12
+ * - fade ladder, level weight and dotAlpha are CPU-baked into ONE per-level
13
+ * alpha (they are per-level constants per frame — magnet-collect owns them).
14
+ * - dot rest radius comes from `GridConfig.dotRadius[0]` (shared config
15
+ * continuity), not the WGSL's hardcoded `halfLen·0.16`.
16
+ * - coincidence skip (§5.4): sites shared with a visible coarser level drop
17
+ * out (integer spacing ratios), replicating the classic max-composite look.
18
+ * - NDC is computed from CSS-px directly (the WGSL path) — glyph sizes are
19
+ * CSS px, DPR rides the viewport; the layer's ortho camera is bypassed.
20
+ */
21
+ import type { GridConfig, GridMagnetConfig } from "../../../core/src/index";
22
+ import { Group } from "three/webgpu";
23
+ import type { GroundFrame } from "../pass";
24
+ import { type MagnetLevel } from "./magnet-collect";
25
+ export interface MagnetGridRenderer {
26
+ readonly group: Group;
27
+ /** Rewrite every uniform + the source buffer from this frame's collect. */
28
+ update(frame: GroundFrame, cfg: GridConfig, magnet: GridMagnetConfig, levels: readonly MagnetLevel[], sources: Float32Array, sourceCount: number): void;
29
+ dispose(): void;
30
+ }
31
+ export declare function createMagnetGrid(): MagnetGridRenderer;
@@ -1,21 +1,10 @@
1
1
  /**
2
- * The dot-grid pass — TSL port of the raw-WebGL grid (itself verbatim from v1
3
- * GridRenderer.ts; the fragment math below is that shader re-expressed as a
4
- * node graph, level loop unrolled in JS). Screen-space: a fullscreen triangle
5
- * whose vertex stage passes clip coords through; the fragment reconstructs
6
- * world position from `screenCoordinate` (top-left device px on BOTH backends
7
- * — three normalizes, so the old manual y-flip is gone) + camera uniforms.
2
+ * Production grid wiring — BUILD-TIME selection, not a runtime mode facade.
8
3
  *
9
- * One deliberate re-expression: the GLSL if/else fade ladder became the
10
- * closed form `clamp(min(rise, fall), 0, 1)` — identical piecewise output
11
- * (rise 0→1 across fadeIn, plateau 1, fall 1→0 across fadeOut) with no
12
- * branching. Everything else (fract-distance dots, 0.5-device-px AA,
13
- * max-composited levels) matches term for term.
4
+ * Keep this module intentionally tiny and import exactly one implementation.
5
+ * The unselected renderer then has no path from the package entry graph and
6
+ * is absent from the production bundle. To restore the classic grid, change
7
+ * only the implementation path below to `./grid-classic-pass`.
14
8
  */
15
- import { type GridConfig } from "../../../core/src/index";
16
- import type { GroundPass } from "../pass";
17
- export interface GridPass extends GroundPass {
18
- /** Live re-tune (the react `grid` prop seam) — partial merge over current values. */
19
- configure(cfg: Partial<GridConfig>): void;
20
- }
21
- export declare function createGridPass(initial?: Partial<GridConfig>): GridPass;
9
+ export { createGridPass } from "./grid-magnet-pass";
10
+ export type { GridPass, GridPassDeps, GridPassFactory } from "./grid-contract";
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Magnet-grid collector — the pure ECS→packed-sources half of the magnet pass
3
+ * (design-010 §4; the `*-collect.ts` convention: imports ONLY core/kernel so
4
+ * it unit-tests under happy-dom with no GPU and no three import).
5
+ *
6
+ * Ported from vibe-field draft/magnet-grid `main.js` (latticeWindow,
7
+ * packCards) with the design-010 upgrades the experiment lacked:
8
+ * - rbush broad-phase over the ONE spatial index instead of an all-cards
9
+ * walk (D2 — document size becomes irrelevant by construction);
10
+ * - N poles from injected sources, packed as DEGENERATE rounded boxes
11
+ * (half = 0, r = 0 — reduces exactly to the point-charge formula, D3);
12
+ * - `maxSources` prioritization (largest screen area, then nearest to the
13
+ * viewport center; poles pack first and are never evicted);
14
+ * - `fadeZoom` — the field quiets below a zoom threshold (scale 0 skips the
15
+ * spatial query entirely: the cheap valve is also the right look).
16
+ */
17
+ import { type Entity, type GridConfig, type GridMagnetConfig, type World } from "../../../core/src/index";
18
+ import { type AABB, type SpatialEntry } from "../../../kernel/src/index";
19
+ import type { GroundFrame } from "../pass";
20
+ import type { Pole } from "../poles";
21
+ /** Floats per packed source: [cx, cy, hx, hy] + [r, strength, 0, 0] (screen px). */
22
+ export declare const MAGNET_SOURCE_FLOATS = 8;
23
+ /** Source-buffer capacity — the scratch array and GPU buffer are sized to this. */
24
+ export declare const MAX_MAGNET_SOURCES = 256;
25
+ /** Per-level instance guard (vibe-field main.js:621 — a level beyond it draws nothing). */
26
+ export declare const MAX_MAGNET_INSTANCES = 220000;
27
+ /** The broad-phase reader over the shared spatial index (ctx seam, design-010 §3.2). */
28
+ export type ReadSpatial = (bounds: AABB) => readonly SpatialEntry<Entity>[];
29
+ /** Resolve the partial config block over the defaults — the ONE resolution point. */
30
+ export declare function resolveMagnet(cfg: GridConfig): GridMagnetConfig;
31
+ /** Closed-form fade ladder (grid.ts:72-74 re-expressed in JS for level skip). */
32
+ export declare function fadeOpacity(cssSpacing: number, fadeIn: readonly [number, number], fadeOut: readonly [number, number]): number;
33
+ /**
34
+ * The field zoom valve: 1 at `zoom ≥ fadeZoom`, lerping to 0 across
35
+ * [0.7·fadeZoom, fadeZoom]. 0 disables the valve entirely.
36
+ */
37
+ export declare function magnetFieldScale(zoom: number, fadeZoom: number): number;
38
+ /** One lattice level's visible window + fade (vibe-field main.js:81-91 port). */
39
+ export interface MagnetLevel {
40
+ /** Integer lattice coords of the window origin. */
41
+ readonly i0: number;
42
+ readonly j0: number;
43
+ readonly cols: number;
44
+ readonly rows: number;
45
+ /** Instances to draw; 0 = level skipped (faded out or over the guard). */
46
+ readonly count: number;
47
+ /** Fade-ladder opacity × level weight (CPU-baked; the shader gets one alpha). */
48
+ readonly alpha: number;
49
+ /**
50
+ * Coincidence skip (design-010 §5.4): when the NEXT level up is visible and
51
+ * its spacing is an integer multiple m of this one, sites whose absolute
52
+ * lattice index is ≡ 0 (mod m) on both axes drop out — replicating the
53
+ * classic shader's max-composite look. 0 = no skip.
54
+ */
55
+ readonly skipModulo: number;
56
+ }
57
+ export declare function collectMagnetLevels(frame: GroundFrame, cfg: GridConfig): MagnetLevel[];
58
+ /**
59
+ * Pack field sources into `out` (stride {@link MAGNET_SOURCE_FLOATS}, screen
60
+ * CSS px). Returns the packed count. `fieldScale` multiplies every strength
61
+ * (the zoom valve); 0 short-circuits to an empty buffer — no spatial query.
62
+ */
63
+ export declare function collectMagnetSources(world: World, frame: GroundFrame, magnet: GridMagnetConfig, fieldScale: number, poles: readonly Pole[], readSpatial: ReadSpatial | undefined, out: Float32Array): number;
@@ -15,7 +15,8 @@
15
15
  * - Materialized port dots: triangle-fan discs, active color while any
16
16
  * connect drag is routed.
17
17
  */
18
- import { type WirePreviewBuffer, type WiresConfig, type World } from "../../../core/src/index";
18
+ import { type Entity, type WirePreviewBuffer, type WiresConfig, type World } from "../../../core/src/index";
19
+ import { type Rect } from "../../../kernel/src/index";
19
20
  import type { GroundFrame } from "../pass";
20
21
  import { type TriSoup } from "./soup-collect";
21
22
  export declare const wireQ: import("@vibecook/strata-ecs").Query;
@@ -28,4 +29,7 @@ export declare const routedConnectQ: import("@vibecook/strata-ecs").Query;
28
29
  export declare const connectDragQ: import("@vibecook/strata-ecs").Query;
29
30
  /** Nav churn signal: Active membership flips re-collect (scope filter below). */
30
31
  export declare const activeQ: import("@vibecook/strata-ecs").Query;
32
+ /** Effective widget rect: non-zero MeasuredSize over Size (design-004 §2).
33
+ * Exported for the magnet collector (design-010 §4) — ONE rect-reading rule. */
34
+ export declare function readWidgetRect(world: World, e: Entity): Rect | undefined;
31
35
  export declare function collectWires(world: World, frame: GroundFrame, config?: WiresConfig, preview?: WirePreviewBuffer): TriSoup;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Magnet pole sources (design-010 §3.3, decision D5): the grid pass is
3
+ * CURSOR-AGNOSTIC — point field sources enter through this injected protocol,
4
+ * never through pass-known vocabulary. No source wired ⇒ no subscription, no
5
+ * reads, widget-only field. The canned helpers below are where any cursor
6
+ * dependency lives; the pass imports NEITHER — apps compose what fits
7
+ * (vibe-field wraps its own `Cur` halo the same way, in its own repo).
8
+ */
9
+ import { type World } from "../../core/src/index";
10
+ /**
11
+ * One point field source. Packed as a degenerate rounded-box SDF (half = 0,
12
+ * r = 0), which reduces exactly to the point-charge formula (design-010 D3).
13
+ * `space` defaults to "world"; "screen" is CSS px — zero-conversion for
14
+ * screen-space cursors (field evaluation is screen-space anyway).
15
+ */
16
+ export interface Pole {
17
+ readonly x: number;
18
+ readonly y: number;
19
+ /** Relative field strength; ≤ 0 poles are skipped at pack time. */
20
+ readonly strength: number;
21
+ readonly space?: "world" | "screen";
22
+ }
23
+ export interface PoleSource {
24
+ /** Called during collect — read-only world access (GroundPass.collect rules). */
25
+ read(world: World): readonly Pole[];
26
+ /**
27
+ * Called during arm — `wake` marks the wired magnet grid dirty. The classic
28
+ * implementation never subscribes. Returns an unsubscriber.
29
+ */
30
+ subscribe(world: World, wake: () => void): () => void;
31
+ }
32
+ /**
33
+ * Poles from the local pointer entity (`LocalPointer` + `PointerWorld`) — the
34
+ * raw-cursor wiring for apps without their own cursor visual. World-space; one
35
+ * pole per local pointer (multi-touch yields one per finger while pressed).
36
+ */
37
+ export declare function localPointerPoles(opts?: {
38
+ readonly strength?: number;
39
+ }): PoleSource;
40
+ /**
41
+ * Poles from cursor-visual entities (`Position` + `CursorVisual`) — presence
42
+ * apps: remote collaborators' pooled cursors (core `remote-cursors.ts`) drive
43
+ * the field. This helper is where the CursorVisual dependency lives (D5).
44
+ */
45
+ export declare function cursorVisualPoles(opts?: {
46
+ readonly strength?: number;
47
+ }): PoleSource;
@@ -35,7 +35,7 @@
35
35
  * `createCanvasEngine({ measureQueue })` (ingest side) and this component's
36
36
  * `measureQueue` prop (ResizeObserver side). Absent ⇒ measurement is skipped.
37
37
  */
38
- import { type CanvasEngine, type GridConfig, type MeasureQueue, type ReflectorDef, type WirePreviewBuffer, type World } from "../../core/src/index";
38
+ import { type CanvasEngine, type Entity, type GridConfig, type MeasureQueue, type ReflectorDef, type WirePreviewBuffer, type World } from "../../core/src/index";
39
39
  import { type CanvasHost, type GLRoute, type Planes, type WidgetFocusHandle } from "../../dom/src/index";
40
40
  import { type CSSProperties, type ReactElement, type ReactNode } from "react";
41
41
  import { type KeymapEntry } from "./keymap";
@@ -72,6 +72,23 @@ export type GroundLayerFactory = (ctx: {
72
72
  };
73
73
  readonly world: World;
74
74
  readonly readWirePreview: () => WirePreviewBuffer;
75
+ /**
76
+ * Broad-phase rect query over the interaction stack's spatial index (the
77
+ * magnet grid's widget sources, design-010 §3.2). Structural mirror of
78
+ * `@ice/ground`'s ReadSpatial — kernel AABB/SpatialEntry shapes inlined.
79
+ */
80
+ readonly readSpatial: (bounds: {
81
+ readonly minX: number;
82
+ readonly minY: number;
83
+ readonly maxX: number;
84
+ readonly maxY: number;
85
+ }) => ReadonlyArray<{
86
+ minX: number;
87
+ minY: number;
88
+ maxX: number;
89
+ maxY: number;
90
+ id: Entity;
91
+ }>;
75
92
  }) => GroundLayerHandle;
76
93
  export interface InfiniteCanvasProps {
77
94
  /** A constructed engine (`createCanvasEngine(...)`); NOT disposed on unmount. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecook/ice",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "ICE — infinite canvas engine. Figma-grade infinite-canvas UX as a framework: defineWidget turns React/R3F components into canvas citizens — selectable, movable, resizable, wired into node graphs, CRDT-synced, undoable per gesture.",
5
5
  "license": "MIT",
6
6
  "author": "James Yong <me@jamesyong42.com>",