@vibecook/ice 0.8.1 → 0.10.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.
@@ -11,6 +11,8 @@
11
11
  import type { GridConfig, SnapGuidesConfig, WirePreviewBuffer, WiresConfig, World } from "../../core/src/index";
12
12
  import { type GroundLayerHost, type GroundReflector } from "./layer";
13
13
  import type { GroundPass } from "./pass";
14
+ import type { ReadSpatial } from "./passes/magnet-collect";
15
+ import type { PoleSource } from "./poles";
14
16
  import { type GroundRendererLike } from "./renderer";
15
17
  export type { GroundFrame, GroundPass } from "./pass";
16
18
  export type { GroundReflector } from "./layer";
@@ -18,12 +20,20 @@ export type { GroundRendererLike } from "./renderer";
18
20
  export { SoupBuilder, parseCssColor, type Rgba, type TriSoup } from "./passes/soup-collect";
19
21
  export { collectGuides } from "./passes/guides-collect";
20
22
  export { collectWires } from "./passes/wires-collect";
21
- export { DEFAULT_GRID_CONFIG, DEFAULT_SNAP_GUIDES_CONFIG, DEFAULT_WIRES_CONFIG, type GridConfig, type SnapGuidesConfig, type WiresConfig, } from "../../core/src/index";
23
+ export { cursorVisualPoles, localPointerPoles, type Pole, type PoleSource } from "./poles";
24
+ export { collectMagnetLevels, collectMagnetSources, magnetFieldScale, resolveMagnet, MAX_MAGNET_SOURCES, type MagnetLevel, type ReadSpatial, } from "./passes/magnet-collect";
25
+ export { DEFAULT_GRID_CONFIG, DEFAULT_GRID_MAGNET_CONFIG, DEFAULT_SNAP_GUIDES_CONFIG, DEFAULT_WIRES_CONFIG, type GridConfig, type GridMagnetConfig, type SnapGuidesConfig, type WiresConfig, } from "../../core/src/index";
22
26
  export interface GroundContext {
23
27
  readonly host: GroundLayerHost;
24
28
  readonly world: World;
25
29
  /** Connect-drag preview reader (graph boards); omit on boards without wires. */
26
30
  readonly readWirePreview?: () => WirePreviewBuffer;
31
+ /**
32
+ * Broad-phase reader over the ONE spatial index (design-010 §3.2, the
33
+ * readWirePreview precedent) — the magnet grid's widget sources. Facades
34
+ * wire `stack.index.search`; omit ⇒ magnet renders a pole-only field.
35
+ */
36
+ readonly readSpatial?: ReadSpatial;
27
37
  }
28
38
  export interface GroundLayer {
29
39
  readonly reflector: GroundReflector;
@@ -35,6 +45,13 @@ export interface GroundOptions {
35
45
  readonly grid?: Partial<GridConfig>;
36
46
  readonly wires?: Partial<WiresConfig>;
37
47
  readonly guides?: Partial<SnapGuidesConfig>;
48
+ /**
49
+ * Magnet pole sources (design-010 §3.3) — live app-authored objects, so they
50
+ * live HERE, not in the plain-data GridConfig. Nothing wired ⇒ the magnet
51
+ * field (if enabled) is widget-only; with magnet off they cost one gated
52
+ * no-op subscription each.
53
+ */
54
+ readonly poles?: PoleSource | readonly PoleSource[];
38
55
  /** Extra app passes, rendered after the built-ins (renderOrder ≥ 3 is yours). */
39
56
  readonly passes?: readonly GroundPass[];
40
57
  /** Force the WebGL2 backend (debug / e2e A-B runs). */
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The CLASSIC dot-grid renderer — extracted VERBATIM from grid.ts when the
3
+ * magnet mode arrived (design-010 D1: classic stays the default and the
4
+ * pixel-exact baseline; the pass facade in grid.ts owns mode switching).
5
+ *
6
+ * TSL port of the raw-WebGL grid (itself verbatim from v1 GridRenderer.ts;
7
+ * the fragment math below is that shader re-expressed as a node graph, level
8
+ * loop unrolled in JS). Screen-space: a fullscreen triangle whose vertex
9
+ * stage passes clip coords through; the fragment reconstructs world position
10
+ * from `screenCoordinate` (top-left device px on BOTH backends — three
11
+ * normalizes, so the old manual y-flip is gone) + camera uniforms.
12
+ *
13
+ * One deliberate re-expression: the GLSL if/else fade ladder became the
14
+ * closed form `clamp(min(rise, fall), 0, 1)` — identical piecewise output
15
+ * (rise 0→1 across fadeIn, plateau 1, fall 1→0 across fadeOut) with no
16
+ * branching. Everything else (fract-distance dots, 0.5-device-px AA,
17
+ * max-composited levels) matches term for term.
18
+ */
19
+ import type { GridConfig } from "../../../core/src/index";
20
+ import { Mesh } from "three/webgpu";
21
+ import type { GroundFrame } from "../pass";
22
+ export interface ClassicGridRenderer {
23
+ readonly mesh: Mesh;
24
+ /** Layer-written each collect. */
25
+ setCamera(frame: GroundFrame): void;
26
+ /** Config-written (the configure seam). */
27
+ configure(cfg: GridConfig): void;
28
+ dispose(): void;
29
+ }
30
+ export declare function createClassicGrid(cfg: GridConfig): ClassicGridRenderer;
@@ -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]` (config continuity
15
+ * with the classic grid), 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,28 @@
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
+ * The grid pass — a MODE FACADE since design-010: the classic analytic dot
3
+ * grid (grid-classic.ts, the default — byte-identical to pre-magnet builds)
4
+ * and the magnet field lattice (grid-magnet.ts + magnet-collect.ts) behind
5
+ * ONE `GridPass` contract. `configure` deep-merges the `magnet` key one level
6
+ * (§3.1) so partial re-tunes never clobber the block.
8
7
  *
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.
8
+ * Wake graph (all magnet wakes gated on `enabled` — the idle-scene zero-frame
9
+ * guarantee is untouched when off): camera/resize are layer-owned;
10
+ * `SpatialVersion` re-collects widget sources (gated on `widgets` too);
11
+ * injected pole sources wake through their own subscriptions (D5 — the pass
12
+ * observes no pointer/cursor/presence vocabulary). The magnet renderer is
13
+ * built LAZILY on first enable: classic-only apps pay zero magnet cost.
14
14
  */
15
15
  import { type GridConfig } from "../../../core/src/index";
16
16
  import type { GroundPass } from "../pass";
17
+ import type { PoleSource } from "../poles";
18
+ import { type ReadSpatial } from "./magnet-collect";
17
19
  export interface GridPass extends GroundPass {
18
20
  /** Live re-tune (the react `grid` prop seam) — partial merge over current values. */
19
21
  configure(cfg: Partial<GridConfig>): void;
20
22
  }
21
- export declare function createGridPass(initial?: Partial<GridConfig>): GridPass;
23
+ /** Injected magnet inputs (design-010 §3.2) — absent pieces disable their sources. */
24
+ export interface GridPassDeps {
25
+ readonly poles?: readonly PoleSource[];
26
+ readonly readSpatial?: ReadSpatial;
27
+ }
28
+ export declare function createGridPass(initial?: Partial<GridConfig>, deps?: GridPassDeps): GridPass;
@@ -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 grid pass dirty (the pass gates it on
28
+ * `magnet.enabled`, so idle scenes stay at zero frames). Returns 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.8.1",
3
+ "version": "0.10.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>",