@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.
package/dist/ground.js CHANGED
@@ -2,12 +2,17 @@ import {
2
2
  Active,
3
3
  Camera,
4
4
  Culled,
5
+ CursorVisual,
5
6
  DEFAULT_GRID_CONFIG,
7
+ DEFAULT_GRID_MAGNET_CONFIG,
6
8
  DEFAULT_SNAP_GUIDES_CONFIG,
7
9
  DEFAULT_WIRES_CONFIG,
8
10
  Drag,
9
11
  GuideLine,
12
+ LocalPointer,
10
13
  MeasuredSize,
14
+ Pointer,
15
+ PointerWorld,
11
16
  Port,
12
17
  PortAnchor,
13
18
  Position,
@@ -16,6 +21,7 @@ import {
16
21
  Selected,
17
22
  Size,
18
23
  SpacingBar,
24
+ SpatialVersion,
19
25
  Wire,
20
26
  WireFrom,
21
27
  WirePorts,
@@ -23,7 +29,7 @@ import {
23
29
  defineQuery,
24
30
  portSlots,
25
31
  widgets
26
- } from "./chunk-LBRHND4G.js";
32
+ } from "./chunk-VGVDJZSP.js";
27
33
  import {
28
34
  portAnchor,
29
35
  tessellateCubic,
@@ -135,96 +141,54 @@ function createLayer(host, world, renderer, passes) {
135
141
  };
136
142
  }
137
143
 
138
- // ../ground/src/passes/grid.ts
144
+ // ../ground/src/passes/grid-contract.ts
145
+ function mergeGridConfig(base, next) {
146
+ const merged = { ...base, ...next };
147
+ if (next.magnet !== void 0) merged.magnet = { ...base.magnet, ...next.magnet };
148
+ return merged;
149
+ }
150
+ function resolveInitialGridConfig(initial) {
151
+ return mergeGridConfig(DEFAULT_GRID_CONFIG, initial);
152
+ }
153
+
154
+ // ../ground/src/passes/grid-magnet.ts
139
155
  import {
156
+ Fn,
157
+ If,
158
+ Loop,
159
+ abs,
140
160
  clamp,
141
161
  float,
142
- fract,
162
+ instanceIndex,
163
+ int,
143
164
  length,
144
165
  max,
145
166
  min,
146
167
  mix,
168
+ mod,
147
169
  positionGeometry,
148
- screenCoordinate,
170
+ select,
171
+ sign,
149
172
  smoothstep,
173
+ sqrt,
174
+ storage,
150
175
  uniform,
151
- vec4
176
+ varying,
177
+ vec2,
178
+ vec4,
179
+ Continue,
180
+ Discard
152
181
  } from "three/tsl";
153
182
  import {
154
183
  BufferAttribute,
155
- BufferGeometry,
184
+ Group,
185
+ InstancedBufferGeometry,
156
186
  Mesh,
157
187
  MeshBasicNodeMaterial,
188
+ StorageBufferAttribute,
158
189
  Vector2,
159
190
  Vector3
160
191
  } from "three/webgpu";
161
- var FULLSCREEN_TRIANGLE = new Float32Array([-1, -1, 0, 3, -1, 0, -1, 3, 0]);
162
- function createGridPass(initial = {}) {
163
- let cfg = { ...DEFAULT_GRID_CONFIG, ...initial };
164
- const uCamera = uniform(new Vector2(0, 0));
165
- const uZoom = uniform(1);
166
- const uDpr = uniform(1);
167
- const uSpacings = uniform(new Vector3(...cfg.spacings));
168
- const uDotColor = uniform(new Vector3(...cfg.dotColor));
169
- const uDotAlpha = uniform(cfg.dotAlpha);
170
- const uFadeIn = uniform(new Vector2(...cfg.fadeIn));
171
- const uFadeOut = uniform(new Vector2(...cfg.fadeOut));
172
- const uDotRadius = uniform(new Vector2(...cfg.dotRadius));
173
- const uLevelWeight = uniform(new Vector2(...cfg.levelWeight));
174
- const effZoom = uZoom.mul(uDpr);
175
- const worldPos = screenCoordinate.xy.div(effZoom).add(uCamera);
176
- let total = float(0);
177
- for (let i = 0; i < 3; i++) {
178
- const spacing = i === 0 ? uSpacings.x : i === 1 ? uSpacings.y : uSpacings.z;
179
- const cssSpacing = spacing.mul(uZoom);
180
- const rise = cssSpacing.sub(uFadeIn.x).div(uFadeIn.y.sub(uFadeIn.x));
181
- const fall = float(1).sub(cssSpacing.sub(uFadeOut.x).div(uFadeOut.y.sub(uFadeOut.x)));
182
- const opacity = clamp(min(rise, fall), 0, 1);
183
- const f = fract(worldPos.div(spacing).add(0.5)).sub(0.5);
184
- const dist = length(f).mul(spacing).mul(effZoom);
185
- const t = clamp(cssSpacing.sub(uFadeIn.x).div(40), 0, 1);
186
- const radius = mix(uDotRadius.x, uDotRadius.y, t).mul(uDpr);
187
- const dot = smoothstep(radius.sub(0.5), radius.add(0.5), dist).oneMinus();
188
- const weight = uLevelWeight.x.add(float(i).mul(uLevelWeight.y));
189
- total = max(total, dot.mul(opacity).mul(weight));
190
- }
191
- const material = new MeshBasicNodeMaterial();
192
- material.vertexNode = vec4(positionGeometry.xy, 0, 1);
193
- material.fragmentNode = vec4(uDotColor, clamp(total.mul(uDotAlpha), 0, 1));
194
- material.transparent = true;
195
- material.depthTest = false;
196
- material.depthWrite = false;
197
- const geometry = new BufferGeometry();
198
- geometry.setAttribute("position", new BufferAttribute(FULLSCREEN_TRIANGLE, 3));
199
- const mesh = new Mesh(geometry, material);
200
- mesh.frustumCulled = false;
201
- mesh.renderOrder = 0;
202
- return {
203
- name: "grid",
204
- object: mesh,
205
- arm: () => [],
206
- // camera/resize dirt is layer-owned; config re-tunes call invalidate
207
- collect(_world, frame) {
208
- uCamera.value.set(frame.camera.x, frame.camera.y);
209
- uZoom.value = frame.camera.zoom;
210
- uDpr.value = frame.dpr;
211
- },
212
- configure(next) {
213
- cfg = { ...cfg, ...next };
214
- uSpacings.value.set(...cfg.spacings);
215
- uDotColor.value.set(...cfg.dotColor);
216
- uDotAlpha.value = cfg.dotAlpha;
217
- uFadeIn.value.set(...cfg.fadeIn);
218
- uFadeOut.value.set(...cfg.fadeOut);
219
- uDotRadius.value.set(...cfg.dotRadius);
220
- uLevelWeight.value.set(...cfg.levelWeight);
221
- },
222
- dispose() {
223
- geometry.dispose();
224
- material.dispose();
225
- }
226
- };
227
- }
228
192
 
229
193
  // ../ground/src/passes/soup-collect.ts
230
194
  var SoupBuilder = class {
@@ -308,134 +272,6 @@ function parseCssColor(css) {
308
272
  return [0.5, 0.5, 0.5, 1];
309
273
  }
310
274
 
311
- // ../ground/src/passes/guides-collect.ts
312
- var guideQ = defineQuery([GuideLine]);
313
- var barQ = defineQuery([SpacingBar]);
314
- function collectGuides(world, frame, config = DEFAULT_SNAP_GUIDES_CONFIG) {
315
- const soup = new SoupBuilder();
316
- const cam = frame.camera;
317
- const lw = config.lineWidth;
318
- const guideColor = [config.color[0], config.color[1], config.color[2], config.guideAlpha];
319
- const barColor = [config.color[0], config.color[1], config.color[2], config.spacingAlpha];
320
- world.query(guideQ).each((b) => {
321
- for (const r of b) {
322
- const g = world.read(b.entity(r), GuideLine);
323
- const fullSpan = g.from === g.to;
324
- if (g.axis === "x") {
325
- const sx = worldToScreen(g.at, 0, cam).x;
326
- if (frame.width > 0 && (sx < -1 || sx > frame.width + 1)) continue;
327
- const y0 = fullSpan ? 0 : worldToScreen(0, g.from, cam).y;
328
- const y1 = fullSpan ? frame.height : worldToScreen(0, g.to, cam).y;
329
- soup.rect(sx - lw / 2, Math.min(y0, y1), lw, Math.abs(y1 - y0), guideColor);
330
- } else {
331
- const sy = worldToScreen(0, g.at, cam).y;
332
- if (frame.height > 0 && (sy < -1 || sy > frame.height + 1)) continue;
333
- const x0 = fullSpan ? 0 : worldToScreen(g.from, 0, cam).x;
334
- const x1 = fullSpan ? frame.width : worldToScreen(g.to, 0, cam).x;
335
- soup.rect(Math.min(x0, x1), sy - lw / 2, Math.abs(x1 - x0), lw, guideColor);
336
- }
337
- }
338
- });
339
- world.query(barQ).each((b) => {
340
- for (const r of b) {
341
- const s = world.read(b.entity(r), SpacingBar);
342
- const t = config.tickPx;
343
- if (s.axis === "x") {
344
- const a = worldToScreen(s.from, s.perp, cam);
345
- const bp = worldToScreen(s.to, s.perp, cam);
346
- if (frame.width > 0 && (Math.max(a.x, bp.x) < 0 || Math.min(a.x, bp.x) > frame.width)) continue;
347
- soup.rect(Math.min(a.x, bp.x), a.y - lw / 2, Math.abs(bp.x - a.x), lw, barColor);
348
- soup.rect(a.x - lw / 2, a.y - t, lw, t * 2, barColor);
349
- soup.rect(bp.x - lw / 2, bp.y - t, lw, t * 2, barColor);
350
- } else {
351
- const a = worldToScreen(s.perp, s.from, cam);
352
- const bp = worldToScreen(s.perp, s.to, cam);
353
- if (frame.height > 0 && (Math.max(a.y, bp.y) < 0 || Math.min(a.y, bp.y) > frame.height)) continue;
354
- soup.rect(a.x - lw / 2, Math.min(a.y, bp.y), lw, Math.abs(bp.y - a.y), barColor);
355
- soup.rect(a.x - t, a.y - lw / 2, t * 2, lw, barColor);
356
- soup.rect(bp.x - t, bp.y - lw / 2, t * 2, lw, barColor);
357
- }
358
- }
359
- });
360
- return soup.build();
361
- }
362
-
363
- // ../ground/src/passes/soup-mesh.ts
364
- import { attribute } from "three/tsl";
365
- import { BufferAttribute as BufferAttribute2, BufferGeometry as BufferGeometry2, DoubleSide, Mesh as Mesh2, MeshBasicNodeMaterial as MeshBasicNodeMaterial2 } from "three/webgpu";
366
- function createSoupMesh(renderOrder) {
367
- const material = new MeshBasicNodeMaterial2();
368
- material.fragmentNode = attribute("color", "vec4");
369
- material.transparent = true;
370
- material.depthTest = false;
371
- material.depthWrite = false;
372
- material.side = DoubleSide;
373
- let geometry = new BufferGeometry2();
374
- let capacity = 0;
375
- const mesh = new Mesh2(geometry, material);
376
- mesh.frustumCulled = false;
377
- mesh.renderOrder = renderOrder;
378
- mesh.visible = false;
379
- return {
380
- mesh,
381
- update(soup) {
382
- if (soup.vertexCount === 0) {
383
- mesh.visible = false;
384
- geometry.setDrawRange(0, 0);
385
- return;
386
- }
387
- if (soup.vertexCount > capacity) {
388
- capacity = Math.max(soup.vertexCount, capacity * 2, 256);
389
- const old = geometry;
390
- geometry = new BufferGeometry2();
391
- const pos = new Float32Array(capacity * 3);
392
- const col = new Float32Array(capacity * 4);
393
- pos.set(soup.positions);
394
- col.set(soup.colors);
395
- geometry.setAttribute("position", new BufferAttribute2(pos, 3));
396
- geometry.setAttribute("color", new BufferAttribute2(col, 4));
397
- mesh.geometry = geometry;
398
- old.dispose();
399
- } else {
400
- const pos = geometry.getAttribute("position");
401
- const col = geometry.getAttribute("color");
402
- pos.array.set(soup.positions);
403
- col.array.set(soup.colors);
404
- pos.needsUpdate = true;
405
- col.needsUpdate = true;
406
- }
407
- geometry.setDrawRange(0, soup.vertexCount);
408
- mesh.visible = true;
409
- },
410
- dispose() {
411
- geometry.dispose();
412
- material.dispose();
413
- }
414
- };
415
- }
416
-
417
- // ../ground/src/passes/guides.ts
418
- function createGuidesPass(initial = {}) {
419
- const config = { ...DEFAULT_SNAP_GUIDES_CONFIG, ...initial };
420
- const soup = createSoupMesh(2);
421
- return {
422
- name: "snap-guides",
423
- object: soup.mesh,
424
- arm(world, wake) {
425
- return [
426
- world.reactive.observeQuery(guideQ, wake, { cols: [GuideLine] }),
427
- world.reactive.observeQuery(barQ, wake, { cols: [SpacingBar] })
428
- ];
429
- },
430
- collect(world, frame) {
431
- soup.update(collectGuides(world, frame, config));
432
- },
433
- dispose() {
434
- soup.dispose();
435
- }
436
- };
437
- }
438
-
439
275
  // ../ground/src/passes/wires-collect.ts
440
276
  var wireQ = defineQuery([Wire]);
441
277
  var geometryQ = defineQuery([Position, Size]);
@@ -451,7 +287,7 @@ function outOfScope(world, e) {
451
287
  var TESSELLATION_TOLERANCE_PX = 0.25;
452
288
  var DASH_PX = 6;
453
289
  var GAP_PX = 4;
454
- function readRect(world, e) {
290
+ function readWidgetRect(world, e) {
455
291
  const p = world.get(e, Position);
456
292
  if (p === void 0) return void 0;
457
293
  const measured = world.get(e, MeasuredSize);
@@ -470,7 +306,7 @@ function slotOf(wt, portId) {
470
306
  }
471
307
  function endpointAnchor(world, widget, portId) {
472
308
  if (widget === void 0) return void 0;
473
- const rect = readRect(world, widget);
309
+ const rect = readWidgetRect(world, widget);
474
310
  if (rect === void 0) return void 0;
475
311
  const type = world.get(widget, PrefabId)?.id;
476
312
  if (typeof type !== "string") return void 0;
@@ -572,6 +408,514 @@ function collectWires(world, frame, config = DEFAULT_WIRES_CONFIG, preview) {
572
408
  return soup.build();
573
409
  }
574
410
 
411
+ // ../ground/src/passes/magnet-collect.ts
412
+ var MAGNET_SOURCE_FLOATS = 8;
413
+ var MAX_MAGNET_SOURCES = 256;
414
+ var MAX_MAGNET_INSTANCES = 22e4;
415
+ function resolveMagnet(cfg) {
416
+ return { ...DEFAULT_GRID_MAGNET_CONFIG, ...cfg.magnet };
417
+ }
418
+ function fadeOpacity(cssSpacing, fadeIn, fadeOut) {
419
+ const rise = (cssSpacing - fadeIn[0]) / Math.max(fadeIn[1] - fadeIn[0], 1e-4);
420
+ const fall = 1 - (cssSpacing - fadeOut[0]) / Math.max(fadeOut[1] - fadeOut[0], 1e-4);
421
+ return Math.min(Math.max(Math.min(rise, fall), 0), 1);
422
+ }
423
+ function magnetFieldScale(zoom, fadeZoom) {
424
+ if (fadeZoom <= 0) return 1;
425
+ const t = (zoom - 0.7 * fadeZoom) / (0.3 * fadeZoom);
426
+ return Math.min(Math.max(t, 0), 1);
427
+ }
428
+ function collectMagnetLevels(frame, cfg) {
429
+ const { camera } = frame;
430
+ const out = [];
431
+ const opacities = cfg.spacings.map((s) => fadeOpacity(s * camera.zoom, cfg.fadeIn, cfg.fadeOut));
432
+ for (let i = 0; i < cfg.spacings.length; i++) {
433
+ const spacing = cfg.spacings[i];
434
+ const opacity = opacities[i];
435
+ const weight = cfg.levelWeight[0] + i * cfg.levelWeight[1];
436
+ const alpha = opacity * weight * cfg.dotAlpha;
437
+ if (opacity < 0.01 || alpha <= 0) {
438
+ out.push({ i0: 0, j0: 0, cols: 0, rows: 0, count: 0, alpha: 0, skipModulo: 0 });
439
+ continue;
440
+ }
441
+ const worldW = frame.width / camera.zoom;
442
+ const worldH = frame.height / camera.zoom;
443
+ const i0 = Math.floor(camera.x / spacing) - 1;
444
+ const j0 = Math.floor(camera.y / spacing) - 1;
445
+ const i1 = Math.ceil((camera.x + worldW) / spacing) + 1;
446
+ const j1 = Math.ceil((camera.y + worldH) / spacing) + 1;
447
+ const cols = Math.max(1, i1 - i0);
448
+ const rows = Math.max(1, j1 - j0);
449
+ const count = cols * rows;
450
+ if (count > MAX_MAGNET_INSTANCES) {
451
+ out.push({ i0: 0, j0: 0, cols: 0, rows: 0, count: 0, alpha: 0, skipModulo: 0 });
452
+ continue;
453
+ }
454
+ let skipModulo = 0;
455
+ const next = cfg.spacings[i + 1];
456
+ if (next !== void 0 && opacities[i + 1] >= 0.01) {
457
+ const ratio = next / spacing;
458
+ if (Number.isInteger(ratio) && ratio > 1) skipModulo = ratio;
459
+ }
460
+ out.push({ i0, j0, cols, rows, count, alpha, skipModulo });
461
+ }
462
+ return out;
463
+ }
464
+ function collectMagnetSources(world, frame, magnet, fieldScale, poles, readSpatial, out) {
465
+ if (fieldScale <= 0) return 0;
466
+ const { camera } = frame;
467
+ const cap = Math.min(magnet.maxSources, MAX_MAGNET_SOURCES, Math.floor(out.length / MAGNET_SOURCE_FLOATS));
468
+ let n = 0;
469
+ for (const pole of poles) {
470
+ if (n >= cap) break;
471
+ if (!(pole.strength > 0)) continue;
472
+ const s = pole.space === "screen" ? pole : worldToScreen(pole.x, pole.y, camera);
473
+ const o = n * MAGNET_SOURCE_FLOATS;
474
+ out[o + 0] = s.x;
475
+ out[o + 1] = s.y;
476
+ out[o + 2] = 0;
477
+ out[o + 3] = 0;
478
+ out[o + 4] = 0;
479
+ out[o + 5] = pole.strength * fieldScale;
480
+ out[o + 6] = 0;
481
+ out[o + 7] = 0;
482
+ n++;
483
+ }
484
+ if (!magnet.widgets || readSpatial === void 0 || magnet.widgetStrength <= 0 || n >= cap) {
485
+ return n;
486
+ }
487
+ const haloCss = 5 * magnet.reach * Math.sqrt(Math.max(magnet.widgetStrength * fieldScale, 1e-4));
488
+ const pad = haloCss / camera.zoom;
489
+ const hits = readSpatial({
490
+ minX: camera.x - pad,
491
+ minY: camera.y - pad,
492
+ maxX: camera.x + frame.width / camera.zoom + pad,
493
+ maxY: camera.y + frame.height / camera.zoom + pad
494
+ });
495
+ const viewCx = frame.width / 2;
496
+ const viewCy = frame.height / 2;
497
+ const candidates = [];
498
+ for (const hit of hits) {
499
+ const rect = readWidgetRect(world, hit.id);
500
+ if (rect === void 0 || rect.width <= 0 || rect.height <= 0) continue;
501
+ const hx = rect.width / 2 * camera.zoom;
502
+ const hy = rect.height / 2 * camera.zoom;
503
+ const center = worldToScreen(rect.x + rect.width / 2, rect.y + rect.height / 2, camera);
504
+ candidates.push({
505
+ cx: center.x,
506
+ cy: center.y,
507
+ hx,
508
+ hy,
509
+ r: Math.min(magnet.widgetRadius * camera.zoom, hx, hy),
510
+ area: hx * hy,
511
+ centerDist: (center.x - viewCx) ** 2 + (center.y - viewCy) ** 2
512
+ });
513
+ }
514
+ const budget = cap - n;
515
+ if (candidates.length > budget) {
516
+ candidates.sort((a, b) => b.area - a.area || a.centerDist - b.centerDist);
517
+ candidates.length = budget;
518
+ }
519
+ const strength = magnet.widgetStrength * fieldScale;
520
+ for (const c of candidates) {
521
+ const o = n * MAGNET_SOURCE_FLOATS;
522
+ out[o + 0] = c.cx;
523
+ out[o + 1] = c.cy;
524
+ out[o + 2] = c.hx;
525
+ out[o + 3] = c.hy;
526
+ out[o + 4] = c.r;
527
+ out[o + 5] = strength;
528
+ out[o + 6] = 0;
529
+ out[o + 7] = 0;
530
+ n++;
531
+ }
532
+ return n;
533
+ }
534
+
535
+ // ../ground/src/passes/grid-magnet.ts
536
+ var CORNERS = new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0]);
537
+ var LEVELS = 3;
538
+ function createMagnetGrid() {
539
+ const sourceArray = new Float32Array(MAX_MAGNET_SOURCES * MAGNET_SOURCE_FLOATS);
540
+ const sourceAttr = new StorageBufferAttribute(sourceArray, 4);
541
+ const sourceBuf = storage(sourceAttr, "vec4", MAX_MAGNET_SOURCES * 2).setPBO(true).toReadOnly();
542
+ const uCam = uniform(new Vector2(0, 0));
543
+ const uZoom = uniform(1);
544
+ const uView = uniform(new Vector2(1, 1));
545
+ const uDotMode = uniform(0);
546
+ const uHalfLen = uniform(5);
547
+ const uHalfWidth = uniform(0.55);
548
+ const uRestRadius = uniform(0.75);
549
+ const uK = uniform(0);
550
+ const uPad = uniform(0);
551
+ const uPolarity = uniform(1);
552
+ const uAlwaysAlign = uniform(0);
553
+ const uCount = uniform(0, "int");
554
+ const uColor = uniform(new Vector3(0.75, 0.77, 0.8));
555
+ const EPS = float(25);
556
+ const REST = vec2(0, 1);
557
+ const sdRoundBox = Fn(([p, b, r]) => {
558
+ const q = abs(p).sub(b).add(vec2(r, r));
559
+ return length(max(q, vec2(0, 0))).add(min(max(q.x, q.y), 0)).sub(r);
560
+ });
561
+ const roundBoxNormal = Fn(([p, b, r]) => {
562
+ const s = sign(p);
563
+ const q = abs(p);
564
+ const inner = max(b.sub(vec2(r, r)), vec2(0, 0));
565
+ const d = q.sub(inner);
566
+ const n = vec2(0, 0).toVar();
567
+ If(d.x.greaterThan(0).and(d.y.greaterThan(0)), () => {
568
+ const len = length(d);
569
+ If(len.greaterThan(1e-4), () => {
570
+ n.assign(s.mul(d.div(len)));
571
+ }).Else(() => {
572
+ n.assign(vec2(s.x, 0));
573
+ });
574
+ }).ElseIf(d.x.greaterThan(d.y), () => {
575
+ n.assign(vec2(s.x, 0));
576
+ }).Else(() => {
577
+ n.assign(vec2(0, s.y));
578
+ });
579
+ return n;
580
+ });
581
+ const safeNormalize = Fn(([v, fallback]) => {
582
+ const n = length(v);
583
+ return select(n.greaterThan(1e-4), v.div(max(n, 1e-4)), fallback);
584
+ });
585
+ const group = new Group();
586
+ const geometries = [];
587
+ const materials = [];
588
+ const meshes = [];
589
+ const levelUniforms = Array.from({ length: LEVELS }, () => ({
590
+ origin: uniform(new Vector2(0, 0)),
591
+ dims: uniform(new Vector2(1, 1)),
592
+ spacing: uniform(20),
593
+ alpha: uniform(0),
594
+ skipMod: uniform(0)
595
+ }));
596
+ for (let level = 0; level < LEVELS; level++) {
597
+ const lu = levelUniforms[level];
598
+ const vUv = varying(vec2(0, 0), `vMagnetUv${level}`);
599
+ const vAlpha = varying(float(0), `vMagnetAlpha${level}`);
600
+ const vertexNode = Fn(() => {
601
+ const iid = float(instanceIndex);
602
+ const cols = lu.dims.x;
603
+ const col = mod(iid, cols).floor().toVar();
604
+ const row = iid.div(cols).floor().toVar();
605
+ const world = lu.origin.add(vec2(col, row)).mul(lu.spacing);
606
+ const screen = world.sub(uCam).mul(uZoom).toVar();
607
+ const field = vec2(0, 0).toVar();
608
+ const minD = float(1e9).toVar();
609
+ Loop({ start: int(0), end: uCount, type: "int", condition: "<" }, ({ i }) => {
610
+ const a = sourceBuf.element(i.mul(2));
611
+ const b = sourceBuf.element(i.mul(2).add(1));
612
+ const center = a.xy;
613
+ const half = a.zw;
614
+ const rad = b.x;
615
+ const strength = b.y;
616
+ const rel = screen.sub(center);
617
+ const reach = uPad.mul(sqrt(max(strength, 1e-4)));
618
+ If(
619
+ abs(rel.x).greaterThan(half.x.add(reach)).or(abs(rel.y).greaterThan(half.y.add(reach))),
620
+ () => {
621
+ Continue();
622
+ }
623
+ );
624
+ const d = sdRoundBox(rel, half, rad);
625
+ minD.assign(min(minD, d));
626
+ const dist = max(d, 0);
627
+ const r2 = dist.mul(dist).add(EPS);
628
+ const outward = roundBoxNormal(rel, half, rad);
629
+ field.subAssign(outward.mul(uK.mul(strength).mul(uPolarity).div(r2)));
630
+ });
631
+ const fieldDir = safeNormalize(field, REST);
632
+ const mag = length(field);
633
+ const influence = select(uDotMode.greaterThan(0.5), mag.div(mag.add(1)), clamp(mag, 0, 1));
634
+ const corner = positionGeometry.xy;
635
+ const dotRadius = mix(uRestRadius, uHalfLen.mul(1.05), influence);
636
+ const dotLocal = vec2(corner.x, corner.y.negate()).mul(dotRadius);
637
+ const dir = select(
638
+ uAlwaysAlign.greaterThan(0.5),
639
+ fieldDir,
640
+ safeNormalize(mix(REST, fieldDir, influence), REST)
641
+ );
642
+ const nrm = vec2(dir.y.negate(), dir.x);
643
+ const nLen = mix(uHalfLen.mul(0.55), uHalfLen.mul(1.35), influence);
644
+ const nWid = mix(uHalfWidth.mul(0.85), uHalfWidth.mul(1.15), influence);
645
+ const needleLocal = nrm.mul(corner.x).mul(nWid).add(dir.mul(corner.y).mul(nLen));
646
+ const local = select(uDotMode.greaterThan(0.5), dotLocal, needleLocal);
647
+ const pos = screen.add(local);
648
+ const restAlpha = select(uDotMode.greaterThan(0.5), float(0.72), float(0.35));
649
+ const fieldAlpha = mix(restAlpha, float(1), influence);
650
+ const alpha = clamp(lu.alpha.mul(fieldAlpha), 0, 1).toVar();
651
+ If(minD.lessThan(-0.5), () => {
652
+ alpha.assign(0);
653
+ });
654
+ const absI = lu.origin.x.add(col);
655
+ const absJ = lu.origin.y.add(row);
656
+ If(
657
+ lu.skipMod.greaterThan(0.5).and(mod(absI, lu.skipMod).lessThan(0.5)).and(mod(absJ, lu.skipMod).lessThan(0.5)),
658
+ () => {
659
+ alpha.assign(0);
660
+ }
661
+ );
662
+ vUv.assign(corner);
663
+ vAlpha.assign(alpha);
664
+ const ndc = vec2(
665
+ pos.x.div(uView.x).mul(2).sub(1),
666
+ float(1).sub(pos.y.div(uView.y).mul(2))
667
+ );
668
+ return vec4(ndc, 0, 1);
669
+ })();
670
+ const fragmentNode = Fn(() => {
671
+ const dotEdge = float(1).sub(smoothstep(0.62, 1, length(vUv)));
672
+ const ax = float(1).sub(smoothstep(0.55, 1, abs(vUv.x)));
673
+ const ay = float(1).sub(smoothstep(0.82, 1, abs(vUv.y)));
674
+ const edge = select(uDotMode.greaterThan(0.5), dotEdge, ax.mul(ay));
675
+ const a = vAlpha.mul(edge);
676
+ Discard(a.lessThan(0.01));
677
+ return vec4(uColor, a);
678
+ })();
679
+ const material = new MeshBasicNodeMaterial();
680
+ material.vertexNode = vertexNode;
681
+ material.fragmentNode = fragmentNode;
682
+ material.transparent = true;
683
+ material.depthTest = false;
684
+ material.depthWrite = false;
685
+ const geometry = new InstancedBufferGeometry();
686
+ geometry.setAttribute("position", new BufferAttribute(CORNERS, 3));
687
+ geometry.instanceCount = 0;
688
+ const mesh = new Mesh(geometry, material);
689
+ mesh.frustumCulled = false;
690
+ mesh.renderOrder = 0;
691
+ mesh.visible = false;
692
+ geometries.push(geometry);
693
+ materials.push(material);
694
+ meshes.push(mesh);
695
+ group.add(mesh);
696
+ }
697
+ return {
698
+ group,
699
+ update(frame, cfg, magnet, levels, sources, sourceCount) {
700
+ uCam.value.set(frame.camera.x, frame.camera.y);
701
+ uZoom.value = frame.camera.zoom;
702
+ uView.value.set(Math.max(1, frame.width), Math.max(1, frame.height));
703
+ uDotMode.value = magnet.glyph === "dot" ? 1 : 0;
704
+ uHalfLen.value = magnet.needleLength;
705
+ uHalfWidth.value = magnet.needleWidth;
706
+ uRestRadius.value = cfg.dotRadius[0];
707
+ const k = 0.5 * magnet.reach * magnet.reach;
708
+ uK.value = k;
709
+ uPad.value = Math.sqrt(Math.max(k, 1) / 0.02);
710
+ uPolarity.value = magnet.polarity;
711
+ uAlwaysAlign.value = magnet.alwaysAlign ? 1 : 0;
712
+ uCount.value = sourceCount;
713
+ uColor.value.set(...cfg.dotColor);
714
+ if (sourceCount > 0) {
715
+ sourceArray.set(sources.subarray(0, sourceCount * MAGNET_SOURCE_FLOATS));
716
+ sourceAttr.needsUpdate = true;
717
+ }
718
+ for (let level = 0; level < LEVELS; level++) {
719
+ const win = levels[level];
720
+ const lu = levelUniforms[level];
721
+ const geometry = geometries[level];
722
+ const mesh = meshes[level];
723
+ if (win === void 0 || win.count <= 0) {
724
+ geometry.instanceCount = 0;
725
+ mesh.visible = false;
726
+ continue;
727
+ }
728
+ lu.origin.value.set(win.i0, win.j0);
729
+ lu.dims.value.set(win.cols, win.rows);
730
+ lu.spacing.value = cfg.spacings[level];
731
+ lu.alpha.value = win.alpha;
732
+ lu.skipMod.value = win.skipModulo;
733
+ geometry.instanceCount = win.count;
734
+ mesh.visible = true;
735
+ }
736
+ },
737
+ dispose() {
738
+ for (const g of geometries) g.dispose();
739
+ for (const m of materials) m.dispose();
740
+ }
741
+ };
742
+ }
743
+
744
+ // ../ground/src/passes/grid-magnet-pass.ts
745
+ var createGridPass = (initial = {}, deps = {}) => {
746
+ let cfg = resolveInitialGridConfig(initial);
747
+ const renderer = createMagnetGrid();
748
+ const poles = deps.poles ?? [];
749
+ const sourceScratch = new Float32Array(MAX_MAGNET_SOURCES * MAGNET_SOURCE_FLOATS);
750
+ const poleScratch = [];
751
+ return {
752
+ name: "grid",
753
+ object: renderer.group,
754
+ arm(world, wake) {
755
+ const wakeIfWidgets = () => {
756
+ const magnet = resolveMagnet(cfg);
757
+ if (magnet.widgets && deps.readSpatial !== void 0) wake();
758
+ };
759
+ const unsubs = [world.reactive.observeResource(SpatialVersion, wakeIfWidgets)];
760
+ for (const source of poles) unsubs.push(source.subscribe(world, wake));
761
+ return unsubs;
762
+ },
763
+ collect(world, frame) {
764
+ const magnet = resolveMagnet(cfg);
765
+ const fieldScale = magnetFieldScale(frame.camera.zoom, magnet.fadeZoom);
766
+ poleScratch.length = 0;
767
+ if (fieldScale > 0) {
768
+ for (const source of poles) poleScratch.push(...source.read(world));
769
+ }
770
+ const sourceCount = collectMagnetSources(
771
+ world,
772
+ frame,
773
+ magnet,
774
+ fieldScale,
775
+ poleScratch,
776
+ deps.readSpatial,
777
+ sourceScratch
778
+ );
779
+ const levels = collectMagnetLevels(frame, cfg);
780
+ renderer.update(frame, cfg, magnet, levels, sourceScratch, sourceCount);
781
+ },
782
+ configure(next) {
783
+ cfg = mergeGridConfig(cfg, next);
784
+ },
785
+ dispose() {
786
+ renderer.dispose();
787
+ }
788
+ };
789
+ };
790
+
791
+ // ../ground/src/passes/guides-collect.ts
792
+ var guideQ = defineQuery([GuideLine]);
793
+ var barQ = defineQuery([SpacingBar]);
794
+ function collectGuides(world, frame, config = DEFAULT_SNAP_GUIDES_CONFIG) {
795
+ const soup = new SoupBuilder();
796
+ const cam = frame.camera;
797
+ const lw = config.lineWidth;
798
+ const guideColor = [config.color[0], config.color[1], config.color[2], config.guideAlpha];
799
+ const barColor = [config.color[0], config.color[1], config.color[2], config.spacingAlpha];
800
+ world.query(guideQ).each((b) => {
801
+ for (const r of b) {
802
+ const g = world.read(b.entity(r), GuideLine);
803
+ const fullSpan = g.from === g.to;
804
+ if (g.axis === "x") {
805
+ const sx = worldToScreen(g.at, 0, cam).x;
806
+ if (frame.width > 0 && (sx < -1 || sx > frame.width + 1)) continue;
807
+ const y0 = fullSpan ? 0 : worldToScreen(0, g.from, cam).y;
808
+ const y1 = fullSpan ? frame.height : worldToScreen(0, g.to, cam).y;
809
+ soup.rect(sx - lw / 2, Math.min(y0, y1), lw, Math.abs(y1 - y0), guideColor);
810
+ } else {
811
+ const sy = worldToScreen(0, g.at, cam).y;
812
+ if (frame.height > 0 && (sy < -1 || sy > frame.height + 1)) continue;
813
+ const x0 = fullSpan ? 0 : worldToScreen(g.from, 0, cam).x;
814
+ const x1 = fullSpan ? frame.width : worldToScreen(g.to, 0, cam).x;
815
+ soup.rect(Math.min(x0, x1), sy - lw / 2, Math.abs(x1 - x0), lw, guideColor);
816
+ }
817
+ }
818
+ });
819
+ world.query(barQ).each((b) => {
820
+ for (const r of b) {
821
+ const s = world.read(b.entity(r), SpacingBar);
822
+ const t = config.tickPx;
823
+ if (s.axis === "x") {
824
+ const a = worldToScreen(s.from, s.perp, cam);
825
+ const bp = worldToScreen(s.to, s.perp, cam);
826
+ if (frame.width > 0 && (Math.max(a.x, bp.x) < 0 || Math.min(a.x, bp.x) > frame.width)) continue;
827
+ soup.rect(Math.min(a.x, bp.x), a.y - lw / 2, Math.abs(bp.x - a.x), lw, barColor);
828
+ soup.rect(a.x - lw / 2, a.y - t, lw, t * 2, barColor);
829
+ soup.rect(bp.x - lw / 2, bp.y - t, lw, t * 2, barColor);
830
+ } else {
831
+ const a = worldToScreen(s.perp, s.from, cam);
832
+ const bp = worldToScreen(s.perp, s.to, cam);
833
+ if (frame.height > 0 && (Math.max(a.y, bp.y) < 0 || Math.min(a.y, bp.y) > frame.height)) continue;
834
+ soup.rect(a.x - lw / 2, Math.min(a.y, bp.y), lw, Math.abs(bp.y - a.y), barColor);
835
+ soup.rect(a.x - t, a.y - lw / 2, t * 2, lw, barColor);
836
+ soup.rect(bp.x - t, bp.y - lw / 2, t * 2, lw, barColor);
837
+ }
838
+ }
839
+ });
840
+ return soup.build();
841
+ }
842
+
843
+ // ../ground/src/passes/soup-mesh.ts
844
+ import { attribute } from "three/tsl";
845
+ import { BufferAttribute as BufferAttribute2, BufferGeometry, DoubleSide, Mesh as Mesh2, MeshBasicNodeMaterial as MeshBasicNodeMaterial2 } from "three/webgpu";
846
+ function createSoupMesh(renderOrder) {
847
+ const material = new MeshBasicNodeMaterial2();
848
+ material.fragmentNode = attribute("color", "vec4");
849
+ material.transparent = true;
850
+ material.depthTest = false;
851
+ material.depthWrite = false;
852
+ material.side = DoubleSide;
853
+ let geometry = new BufferGeometry();
854
+ let capacity = 0;
855
+ const mesh = new Mesh2(geometry, material);
856
+ mesh.frustumCulled = false;
857
+ mesh.renderOrder = renderOrder;
858
+ mesh.visible = false;
859
+ return {
860
+ mesh,
861
+ update(soup) {
862
+ if (soup.vertexCount === 0) {
863
+ mesh.visible = false;
864
+ geometry.setDrawRange(0, 0);
865
+ return;
866
+ }
867
+ if (soup.vertexCount > capacity) {
868
+ capacity = Math.max(soup.vertexCount, capacity * 2, 256);
869
+ const old = geometry;
870
+ geometry = new BufferGeometry();
871
+ const pos = new Float32Array(capacity * 3);
872
+ const col = new Float32Array(capacity * 4);
873
+ pos.set(soup.positions);
874
+ col.set(soup.colors);
875
+ geometry.setAttribute("position", new BufferAttribute2(pos, 3));
876
+ geometry.setAttribute("color", new BufferAttribute2(col, 4));
877
+ mesh.geometry = geometry;
878
+ old.dispose();
879
+ } else {
880
+ const pos = geometry.getAttribute("position");
881
+ const col = geometry.getAttribute("color");
882
+ pos.array.set(soup.positions);
883
+ col.array.set(soup.colors);
884
+ pos.needsUpdate = true;
885
+ col.needsUpdate = true;
886
+ }
887
+ geometry.setDrawRange(0, soup.vertexCount);
888
+ mesh.visible = true;
889
+ },
890
+ dispose() {
891
+ geometry.dispose();
892
+ material.dispose();
893
+ }
894
+ };
895
+ }
896
+
897
+ // ../ground/src/passes/guides.ts
898
+ function createGuidesPass(initial = {}) {
899
+ const config = { ...DEFAULT_SNAP_GUIDES_CONFIG, ...initial };
900
+ const soup = createSoupMesh(2);
901
+ return {
902
+ name: "snap-guides",
903
+ object: soup.mesh,
904
+ arm(world, wake) {
905
+ return [
906
+ world.reactive.observeQuery(guideQ, wake, { cols: [GuideLine] }),
907
+ world.reactive.observeQuery(barQ, wake, { cols: [SpacingBar] })
908
+ ];
909
+ },
910
+ collect(world, frame) {
911
+ soup.update(collectGuides(world, frame, config));
912
+ },
913
+ dispose() {
914
+ soup.dispose();
915
+ }
916
+ };
917
+ }
918
+
575
919
  // ../ground/src/passes/wires.ts
576
920
  function createWiresPass(initial = {}, readPreview) {
577
921
  const config = { ...DEFAULT_WIRES_CONFIG, ...initial };
@@ -682,12 +1026,53 @@ function createGroundRenderer(doc, opts = {}) {
682
1026
  };
683
1027
  }
684
1028
 
1029
+ // ../ground/src/poles.ts
1030
+ var localPointerQ = defineQuery([Pointer, PointerWorld, LocalPointer]);
1031
+ function localPointerPoles(opts = {}) {
1032
+ const strength = opts.strength ?? 1;
1033
+ return {
1034
+ read(world) {
1035
+ const out = [];
1036
+ world.query(localPointerQ).each((b) => {
1037
+ for (const r of b) {
1038
+ const p = world.read(b.entity(r), PointerWorld);
1039
+ out.push({ x: p.x, y: p.y, strength, space: "world" });
1040
+ }
1041
+ });
1042
+ return out;
1043
+ },
1044
+ subscribe(world, wake) {
1045
+ return world.reactive.observeQuery(localPointerQ, wake, { cols: [PointerWorld] });
1046
+ }
1047
+ };
1048
+ }
1049
+ var cursorVisualQ = defineQuery([Position, CursorVisual]);
1050
+ function cursorVisualPoles(opts = {}) {
1051
+ const strength = opts.strength ?? 1;
1052
+ return {
1053
+ read(world) {
1054
+ const out = [];
1055
+ world.query(cursorVisualQ).each((b) => {
1056
+ for (const r of b) {
1057
+ const p = world.read(b.entity(r), Position);
1058
+ out.push({ x: p.x, y: p.y, strength, space: "world" });
1059
+ }
1060
+ });
1061
+ return out;
1062
+ },
1063
+ subscribe(world, wake) {
1064
+ return world.reactive.observeQuery(cursorVisualQ, wake, { cols: [Position] });
1065
+ }
1066
+ };
1067
+ }
1068
+
685
1069
  // ../ground/src/index.ts
686
1070
  function ground(opts = {}) {
687
1071
  return (ctx) => {
688
1072
  const doc = ctx.host.container.ownerDocument;
689
1073
  const renderer = opts.rendererOverride ?? createGroundRenderer(doc, { forceWebGL: opts.forceWebGL === true });
690
- const grid = createGridPass(opts.grid ?? {});
1074
+ const poles = opts.poles === void 0 ? [] : Array.isArray(opts.poles) ? opts.poles : [opts.poles];
1075
+ const grid = createGridPass(opts.grid ?? {}, { poles, ...ctx.readSpatial !== void 0 ? { readSpatial: ctx.readSpatial } : {} });
691
1076
  const wires = createWiresPass(opts.wires ?? {}, ctx.readWirePreview);
692
1077
  const guides = createGuidesPass(opts.guides ?? {});
693
1078
  const passes = [grid, wires, guides, ...opts.passes ?? []];
@@ -704,12 +1089,20 @@ function ground(opts = {}) {
704
1089
  }
705
1090
  export {
706
1091
  DEFAULT_GRID_CONFIG,
1092
+ DEFAULT_GRID_MAGNET_CONFIG,
707
1093
  DEFAULT_SNAP_GUIDES_CONFIG,
708
1094
  DEFAULT_WIRES_CONFIG,
1095
+ MAX_MAGNET_SOURCES,
709
1096
  SoupBuilder,
710
1097
  collectGuides,
1098
+ collectMagnetLevels,
1099
+ collectMagnetSources,
711
1100
  collectWires,
1101
+ cursorVisualPoles,
712
1102
  ground,
713
- parseCssColor
1103
+ localPointerPoles,
1104
+ magnetFieldScale,
1105
+ parseCssColor,
1106
+ resolveMagnet
714
1107
  };
715
1108
  //# sourceMappingURL=ground.js.map