@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.
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-VITC7U3J.js";
32
+ } from "./chunk-5FQS7V2C.js";
27
33
  import {
28
34
  portAnchor,
29
35
  tessellateCubic,
@@ -136,6 +142,9 @@ function createLayer(host, world, renderer, passes) {
136
142
  }
137
143
 
138
144
  // ../ground/src/passes/grid.ts
145
+ import { Group as Group2 } from "three/webgpu";
146
+
147
+ // ../ground/src/passes/grid-classic.ts
139
148
  import {
140
149
  clamp,
141
150
  float,
@@ -159,8 +168,7 @@ import {
159
168
  Vector3
160
169
  } from "three/webgpu";
161
170
  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 };
171
+ function createClassicGrid(cfg) {
164
172
  const uCamera = uniform(new Vector2(0, 0));
165
173
  const uZoom = uniform(1);
166
174
  const uDpr = uniform(1);
@@ -200,24 +208,20 @@ function createGridPass(initial = {}) {
200
208
  mesh.frustumCulled = false;
201
209
  mesh.renderOrder = 0;
202
210
  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) {
211
+ mesh,
212
+ setCamera(frame) {
208
213
  uCamera.value.set(frame.camera.x, frame.camera.y);
209
214
  uZoom.value = frame.camera.zoom;
210
215
  uDpr.value = frame.dpr;
211
216
  },
212
217
  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);
218
+ uSpacings.value.set(...next.spacings);
219
+ uDotColor.value.set(...next.dotColor);
220
+ uDotAlpha.value = next.dotAlpha;
221
+ uFadeIn.value.set(...next.fadeIn);
222
+ uFadeOut.value.set(...next.fadeOut);
223
+ uDotRadius.value.set(...next.dotRadius);
224
+ uLevelWeight.value.set(...next.levelWeight);
221
225
  },
222
226
  dispose() {
223
227
  geometry.dispose();
@@ -226,6 +230,45 @@ function createGridPass(initial = {}) {
226
230
  };
227
231
  }
228
232
 
233
+ // ../ground/src/passes/grid-magnet.ts
234
+ import {
235
+ Fn,
236
+ If,
237
+ Loop,
238
+ abs,
239
+ clamp as clamp2,
240
+ float as float2,
241
+ instanceIndex,
242
+ int,
243
+ length as length2,
244
+ max as max2,
245
+ min as min2,
246
+ mix as mix2,
247
+ mod,
248
+ positionGeometry as positionGeometry2,
249
+ select,
250
+ sign,
251
+ smoothstep as smoothstep2,
252
+ sqrt,
253
+ storage,
254
+ uniform as uniform2,
255
+ varying,
256
+ vec2,
257
+ vec4 as vec42,
258
+ Continue,
259
+ Discard
260
+ } from "three/tsl";
261
+ import {
262
+ BufferAttribute as BufferAttribute2,
263
+ Group,
264
+ InstancedBufferGeometry,
265
+ Mesh as Mesh2,
266
+ MeshBasicNodeMaterial as MeshBasicNodeMaterial2,
267
+ StorageBufferAttribute,
268
+ Vector2 as Vector22,
269
+ Vector3 as Vector32
270
+ } from "three/webgpu";
271
+
229
272
  // ../ground/src/passes/soup-collect.ts
230
273
  var SoupBuilder = class {
231
274
  pos = [];
@@ -308,134 +351,6 @@ function parseCssColor(css) {
308
351
  return [0.5, 0.5, 0.5, 1];
309
352
  }
310
353
 
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
354
  // ../ground/src/passes/wires-collect.ts
440
355
  var wireQ = defineQuery([Wire]);
441
356
  var geometryQ = defineQuery([Position, Size]);
@@ -451,7 +366,7 @@ function outOfScope(world, e) {
451
366
  var TESSELLATION_TOLERANCE_PX = 0.25;
452
367
  var DASH_PX = 6;
453
368
  var GAP_PX = 4;
454
- function readRect(world, e) {
369
+ function readWidgetRect(world, e) {
455
370
  const p = world.get(e, Position);
456
371
  if (p === void 0) return void 0;
457
372
  const measured = world.get(e, MeasuredSize);
@@ -470,7 +385,7 @@ function slotOf(wt, portId) {
470
385
  }
471
386
  function endpointAnchor(world, widget, portId) {
472
387
  if (widget === void 0) return void 0;
473
- const rect = readRect(world, widget);
388
+ const rect = readWidgetRect(world, widget);
474
389
  if (rect === void 0) return void 0;
475
390
  const type = world.get(widget, PrefabId)?.id;
476
391
  if (typeof type !== "string") return void 0;
@@ -572,6 +487,540 @@ function collectWires(world, frame, config = DEFAULT_WIRES_CONFIG, preview) {
572
487
  return soup.build();
573
488
  }
574
489
 
490
+ // ../ground/src/passes/magnet-collect.ts
491
+ var MAGNET_SOURCE_FLOATS = 8;
492
+ var MAX_MAGNET_SOURCES = 256;
493
+ var MAX_MAGNET_INSTANCES = 22e4;
494
+ function resolveMagnet(cfg) {
495
+ return { ...DEFAULT_GRID_MAGNET_CONFIG, ...cfg.magnet };
496
+ }
497
+ function fadeOpacity(cssSpacing, fadeIn, fadeOut) {
498
+ const rise = (cssSpacing - fadeIn[0]) / Math.max(fadeIn[1] - fadeIn[0], 1e-4);
499
+ const fall = 1 - (cssSpacing - fadeOut[0]) / Math.max(fadeOut[1] - fadeOut[0], 1e-4);
500
+ return Math.min(Math.max(Math.min(rise, fall), 0), 1);
501
+ }
502
+ function magnetFieldScale(zoom, fadeZoom) {
503
+ if (fadeZoom <= 0) return 1;
504
+ const t = (zoom - 0.7 * fadeZoom) / (0.3 * fadeZoom);
505
+ return Math.min(Math.max(t, 0), 1);
506
+ }
507
+ function collectMagnetLevels(frame, cfg) {
508
+ const { camera } = frame;
509
+ const out = [];
510
+ const opacities = cfg.spacings.map((s) => fadeOpacity(s * camera.zoom, cfg.fadeIn, cfg.fadeOut));
511
+ for (let i = 0; i < cfg.spacings.length; i++) {
512
+ const spacing = cfg.spacings[i];
513
+ const opacity = opacities[i];
514
+ const weight = cfg.levelWeight[0] + i * cfg.levelWeight[1];
515
+ const alpha = opacity * weight * cfg.dotAlpha;
516
+ if (opacity < 0.01 || alpha <= 0) {
517
+ out.push({ i0: 0, j0: 0, cols: 0, rows: 0, count: 0, alpha: 0, skipModulo: 0 });
518
+ continue;
519
+ }
520
+ const worldW = frame.width / camera.zoom;
521
+ const worldH = frame.height / camera.zoom;
522
+ const i0 = Math.floor(camera.x / spacing) - 1;
523
+ const j0 = Math.floor(camera.y / spacing) - 1;
524
+ const i1 = Math.ceil((camera.x + worldW) / spacing) + 1;
525
+ const j1 = Math.ceil((camera.y + worldH) / spacing) + 1;
526
+ const cols = Math.max(1, i1 - i0);
527
+ const rows = Math.max(1, j1 - j0);
528
+ const count = cols * rows;
529
+ if (count > MAX_MAGNET_INSTANCES) {
530
+ out.push({ i0: 0, j0: 0, cols: 0, rows: 0, count: 0, alpha: 0, skipModulo: 0 });
531
+ continue;
532
+ }
533
+ let skipModulo = 0;
534
+ const next = cfg.spacings[i + 1];
535
+ if (next !== void 0 && opacities[i + 1] >= 0.01) {
536
+ const ratio = next / spacing;
537
+ if (Number.isInteger(ratio) && ratio > 1) skipModulo = ratio;
538
+ }
539
+ out.push({ i0, j0, cols, rows, count, alpha, skipModulo });
540
+ }
541
+ return out;
542
+ }
543
+ function collectMagnetSources(world, frame, magnet, fieldScale, poles, readSpatial, out) {
544
+ if (fieldScale <= 0) return 0;
545
+ const { camera } = frame;
546
+ const cap = Math.min(magnet.maxSources, MAX_MAGNET_SOURCES, Math.floor(out.length / MAGNET_SOURCE_FLOATS));
547
+ let n = 0;
548
+ for (const pole of poles) {
549
+ if (n >= cap) break;
550
+ if (!(pole.strength > 0)) continue;
551
+ const s = pole.space === "screen" ? pole : worldToScreen(pole.x, pole.y, camera);
552
+ const o = n * MAGNET_SOURCE_FLOATS;
553
+ out[o + 0] = s.x;
554
+ out[o + 1] = s.y;
555
+ out[o + 2] = 0;
556
+ out[o + 3] = 0;
557
+ out[o + 4] = 0;
558
+ out[o + 5] = pole.strength * fieldScale;
559
+ out[o + 6] = 0;
560
+ out[o + 7] = 0;
561
+ n++;
562
+ }
563
+ if (!magnet.widgets || readSpatial === void 0 || magnet.widgetStrength <= 0 || n >= cap) {
564
+ return n;
565
+ }
566
+ const haloCss = 5 * magnet.reach * Math.sqrt(Math.max(magnet.widgetStrength * fieldScale, 1e-4));
567
+ const pad = haloCss / camera.zoom;
568
+ const hits = readSpatial({
569
+ minX: camera.x - pad,
570
+ minY: camera.y - pad,
571
+ maxX: camera.x + frame.width / camera.zoom + pad,
572
+ maxY: camera.y + frame.height / camera.zoom + pad
573
+ });
574
+ const viewCx = frame.width / 2;
575
+ const viewCy = frame.height / 2;
576
+ const candidates = [];
577
+ for (const hit of hits) {
578
+ const rect = readWidgetRect(world, hit.id);
579
+ if (rect === void 0 || rect.width <= 0 || rect.height <= 0) continue;
580
+ const hx = rect.width / 2 * camera.zoom;
581
+ const hy = rect.height / 2 * camera.zoom;
582
+ const center = worldToScreen(rect.x + rect.width / 2, rect.y + rect.height / 2, camera);
583
+ candidates.push({
584
+ cx: center.x,
585
+ cy: center.y,
586
+ hx,
587
+ hy,
588
+ r: Math.min(magnet.widgetRadius * camera.zoom, hx, hy),
589
+ area: hx * hy,
590
+ centerDist: (center.x - viewCx) ** 2 + (center.y - viewCy) ** 2
591
+ });
592
+ }
593
+ const budget = cap - n;
594
+ if (candidates.length > budget) {
595
+ candidates.sort((a, b) => b.area - a.area || a.centerDist - b.centerDist);
596
+ candidates.length = budget;
597
+ }
598
+ const strength = magnet.widgetStrength * fieldScale;
599
+ for (const c of candidates) {
600
+ const o = n * MAGNET_SOURCE_FLOATS;
601
+ out[o + 0] = c.cx;
602
+ out[o + 1] = c.cy;
603
+ out[o + 2] = c.hx;
604
+ out[o + 3] = c.hy;
605
+ out[o + 4] = c.r;
606
+ out[o + 5] = strength;
607
+ out[o + 6] = 0;
608
+ out[o + 7] = 0;
609
+ n++;
610
+ }
611
+ return n;
612
+ }
613
+
614
+ // ../ground/src/passes/grid-magnet.ts
615
+ var CORNERS = new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0]);
616
+ var LEVELS = 3;
617
+ function createMagnetGrid() {
618
+ const sourceArray = new Float32Array(MAX_MAGNET_SOURCES * MAGNET_SOURCE_FLOATS);
619
+ const sourceAttr = new StorageBufferAttribute(sourceArray, 4);
620
+ const sourceBuf = storage(sourceAttr, "vec4", MAX_MAGNET_SOURCES * 2).setPBO(true).toReadOnly();
621
+ const uCam = uniform2(new Vector22(0, 0));
622
+ const uZoom = uniform2(1);
623
+ const uView = uniform2(new Vector22(1, 1));
624
+ const uDotMode = uniform2(0);
625
+ const uHalfLen = uniform2(5);
626
+ const uHalfWidth = uniform2(0.55);
627
+ const uRestRadius = uniform2(0.75);
628
+ const uK = uniform2(0);
629
+ const uPad = uniform2(0);
630
+ const uPolarity = uniform2(1);
631
+ const uAlwaysAlign = uniform2(0);
632
+ const uCount = uniform2(0, "int");
633
+ const uColor = uniform2(new Vector32(0.75, 0.77, 0.8));
634
+ const EPS = float2(25);
635
+ const REST = vec2(0, 1);
636
+ const sdRoundBox = Fn(([p, b, r]) => {
637
+ const q = abs(p).sub(b).add(vec2(r, r));
638
+ return length2(max2(q, vec2(0, 0))).add(min2(max2(q.x, q.y), 0)).sub(r);
639
+ });
640
+ const roundBoxNormal = Fn(([p, b, r]) => {
641
+ const s = sign(p);
642
+ const q = abs(p);
643
+ const inner = max2(b.sub(vec2(r, r)), vec2(0, 0));
644
+ const d = q.sub(inner);
645
+ const n = vec2(0, 0).toVar();
646
+ If(d.x.greaterThan(0).and(d.y.greaterThan(0)), () => {
647
+ const len = length2(d);
648
+ If(len.greaterThan(1e-4), () => {
649
+ n.assign(s.mul(d.div(len)));
650
+ }).Else(() => {
651
+ n.assign(vec2(s.x, 0));
652
+ });
653
+ }).ElseIf(d.x.greaterThan(d.y), () => {
654
+ n.assign(vec2(s.x, 0));
655
+ }).Else(() => {
656
+ n.assign(vec2(0, s.y));
657
+ });
658
+ return n;
659
+ });
660
+ const safeNormalize = Fn(([v, fallback]) => {
661
+ const n = length2(v);
662
+ return select(n.greaterThan(1e-4), v.div(max2(n, 1e-4)), fallback);
663
+ });
664
+ const group = new Group();
665
+ const geometries = [];
666
+ const materials = [];
667
+ const meshes = [];
668
+ const levelUniforms = Array.from({ length: LEVELS }, () => ({
669
+ origin: uniform2(new Vector22(0, 0)),
670
+ dims: uniform2(new Vector22(1, 1)),
671
+ spacing: uniform2(20),
672
+ alpha: uniform2(0),
673
+ skipMod: uniform2(0)
674
+ }));
675
+ for (let level = 0; level < LEVELS; level++) {
676
+ const lu = levelUniforms[level];
677
+ const vUv = varying(vec2(0, 0), `vMagnetUv${level}`);
678
+ const vAlpha = varying(float2(0), `vMagnetAlpha${level}`);
679
+ const vertexNode = Fn(() => {
680
+ const iid = float2(instanceIndex);
681
+ const cols = lu.dims.x;
682
+ const col = mod(iid, cols).floor();
683
+ const row = iid.div(cols).floor();
684
+ const world = lu.origin.add(vec2(col, row)).mul(lu.spacing);
685
+ const screen = world.sub(uCam).mul(uZoom);
686
+ const field = vec2(0, 0).toVar();
687
+ const minD = float2(1e9).toVar();
688
+ Loop({ start: int(0), end: uCount, type: "int", condition: "<" }, ({ i }) => {
689
+ const a = sourceBuf.element(i.mul(2));
690
+ const b = sourceBuf.element(i.mul(2).add(1));
691
+ const center = a.xy;
692
+ const half = a.zw;
693
+ const rad = b.x;
694
+ const strength = b.y;
695
+ const rel = screen.sub(center);
696
+ const reach = uPad.mul(sqrt(max2(strength, 1e-4)));
697
+ If(
698
+ abs(rel.x).greaterThan(half.x.add(reach)).or(abs(rel.y).greaterThan(half.y.add(reach))),
699
+ () => {
700
+ Continue();
701
+ }
702
+ );
703
+ const d = sdRoundBox(rel, half, rad);
704
+ minD.assign(min2(minD, d));
705
+ const dist = max2(d, 0);
706
+ const r2 = dist.mul(dist).add(EPS);
707
+ const outward = roundBoxNormal(rel, half, rad);
708
+ field.subAssign(outward.mul(uK.mul(strength).mul(uPolarity).div(r2)));
709
+ });
710
+ const fieldDir = safeNormalize(field, REST);
711
+ const mag = length2(field);
712
+ const influence = select(uDotMode.greaterThan(0.5), mag.div(mag.add(1)), clamp2(mag, 0, 1));
713
+ const corner = positionGeometry2.xy;
714
+ const dotRadius = mix2(uRestRadius, uHalfLen.mul(1.05), influence);
715
+ const dotLocal = corner.mul(dotRadius);
716
+ const dir = select(
717
+ uAlwaysAlign.greaterThan(0.5),
718
+ fieldDir,
719
+ safeNormalize(mix2(REST, fieldDir, influence), REST)
720
+ );
721
+ const nrm = vec2(dir.y.negate(), dir.x);
722
+ const nLen = mix2(uHalfLen.mul(0.55), uHalfLen.mul(1.35), influence);
723
+ const nWid = mix2(uHalfWidth.mul(0.85), uHalfWidth.mul(1.15), influence);
724
+ const needleLocal = nrm.mul(corner.x).mul(nWid).add(dir.mul(corner.y).mul(nLen));
725
+ const local = select(uDotMode.greaterThan(0.5), dotLocal, needleLocal);
726
+ const pos = screen.add(local);
727
+ const restAlpha = select(uDotMode.greaterThan(0.5), float2(0.72), float2(0.35));
728
+ const fieldAlpha = mix2(restAlpha, float2(1), influence);
729
+ const alpha = clamp2(lu.alpha.mul(fieldAlpha), 0, 1).toVar();
730
+ If(minD.lessThan(-0.5), () => {
731
+ alpha.assign(0);
732
+ });
733
+ const absI = lu.origin.x.add(col);
734
+ const absJ = lu.origin.y.add(row);
735
+ If(
736
+ lu.skipMod.greaterThan(0.5).and(mod(absI, lu.skipMod).lessThan(0.5)).and(mod(absJ, lu.skipMod).lessThan(0.5)),
737
+ () => {
738
+ alpha.assign(0);
739
+ }
740
+ );
741
+ vUv.assign(corner);
742
+ vAlpha.assign(alpha);
743
+ const ndc = vec2(
744
+ pos.x.div(uView.x).mul(2).sub(1),
745
+ float2(1).sub(pos.y.div(uView.y).mul(2))
746
+ );
747
+ return vec42(ndc, 0, 1);
748
+ })();
749
+ const fragmentNode = Fn(() => {
750
+ const dotEdge = float2(1).sub(smoothstep2(0.62, 1, length2(vUv)));
751
+ const ax = float2(1).sub(smoothstep2(0.55, 1, abs(vUv.x)));
752
+ const ay = float2(1).sub(smoothstep2(0.82, 1, abs(vUv.y)));
753
+ const edge = select(uDotMode.greaterThan(0.5), dotEdge, ax.mul(ay));
754
+ const a = vAlpha.mul(edge);
755
+ Discard(a.lessThan(0.01));
756
+ return vec42(uColor, a);
757
+ })();
758
+ const material = new MeshBasicNodeMaterial2();
759
+ material.vertexNode = vertexNode;
760
+ material.fragmentNode = fragmentNode;
761
+ material.transparent = true;
762
+ material.depthTest = false;
763
+ material.depthWrite = false;
764
+ const geometry = new InstancedBufferGeometry();
765
+ geometry.setAttribute("position", new BufferAttribute2(CORNERS, 3));
766
+ geometry.instanceCount = 0;
767
+ const mesh = new Mesh2(geometry, material);
768
+ mesh.frustumCulled = false;
769
+ mesh.renderOrder = 0;
770
+ mesh.visible = false;
771
+ geometries.push(geometry);
772
+ materials.push(material);
773
+ meshes.push(mesh);
774
+ group.add(mesh);
775
+ }
776
+ return {
777
+ group,
778
+ update(frame, cfg, magnet, levels, sources, sourceCount) {
779
+ uCam.value.set(frame.camera.x, frame.camera.y);
780
+ uZoom.value = frame.camera.zoom;
781
+ uView.value.set(Math.max(1, frame.width), Math.max(1, frame.height));
782
+ uDotMode.value = magnet.glyph === "dot" ? 1 : 0;
783
+ uHalfLen.value = magnet.needleLength;
784
+ uHalfWidth.value = magnet.needleWidth;
785
+ uRestRadius.value = cfg.dotRadius[0];
786
+ const k = 0.5 * magnet.reach * magnet.reach;
787
+ uK.value = k;
788
+ uPad.value = Math.sqrt(Math.max(k, 1) / 0.02);
789
+ uPolarity.value = magnet.polarity;
790
+ uAlwaysAlign.value = magnet.alwaysAlign ? 1 : 0;
791
+ uCount.value = sourceCount;
792
+ uColor.value.set(...cfg.dotColor);
793
+ if (sourceCount > 0) {
794
+ sourceArray.set(sources.subarray(0, sourceCount * MAGNET_SOURCE_FLOATS));
795
+ sourceAttr.needsUpdate = true;
796
+ }
797
+ for (let level = 0; level < LEVELS; level++) {
798
+ const win = levels[level];
799
+ const lu = levelUniforms[level];
800
+ const geometry = geometries[level];
801
+ const mesh = meshes[level];
802
+ if (win === void 0 || win.count <= 0) {
803
+ geometry.instanceCount = 0;
804
+ mesh.visible = false;
805
+ continue;
806
+ }
807
+ lu.origin.value.set(win.i0, win.j0);
808
+ lu.dims.value.set(win.cols, win.rows);
809
+ lu.spacing.value = cfg.spacings[level];
810
+ lu.alpha.value = win.alpha;
811
+ lu.skipMod.value = win.skipModulo;
812
+ geometry.instanceCount = win.count;
813
+ mesh.visible = true;
814
+ }
815
+ },
816
+ dispose() {
817
+ for (const g of geometries) g.dispose();
818
+ for (const m of materials) m.dispose();
819
+ }
820
+ };
821
+ }
822
+
823
+ // ../ground/src/passes/grid.ts
824
+ function mergeGridConfig(base, next) {
825
+ const merged = { ...base, ...next };
826
+ if (next.magnet !== void 0) merged.magnet = { ...base.magnet, ...next.magnet };
827
+ return merged;
828
+ }
829
+ function createGridPass(initial = {}, deps = {}) {
830
+ let cfg = mergeGridConfig(DEFAULT_GRID_CONFIG, initial);
831
+ const group = new Group2();
832
+ const classic = createClassicGrid(cfg);
833
+ group.add(classic.mesh);
834
+ let magnet = null;
835
+ const poles = deps.poles ?? [];
836
+ const sourceScratch = new Float32Array(MAX_MAGNET_SOURCES * MAGNET_SOURCE_FLOATS);
837
+ const poleScratch = [];
838
+ return {
839
+ name: "grid",
840
+ object: group,
841
+ arm(world, wake) {
842
+ const wakeIfWidgets = () => {
843
+ const m = resolveMagnet(cfg);
844
+ if (m.enabled && m.widgets && deps.readSpatial !== void 0) wake();
845
+ };
846
+ const wakeIfEnabled = () => {
847
+ if (resolveMagnet(cfg).enabled) wake();
848
+ };
849
+ const unsubs = [world.reactive.observeResource(SpatialVersion, wakeIfWidgets)];
850
+ for (const source of poles) unsubs.push(source.subscribe(world, wakeIfEnabled));
851
+ return unsubs;
852
+ },
853
+ collect(world, frame) {
854
+ const m = resolveMagnet(cfg);
855
+ if (!m.enabled) {
856
+ classic.mesh.visible = true;
857
+ if (magnet !== null) magnet.group.visible = false;
858
+ classic.setCamera(frame);
859
+ return;
860
+ }
861
+ if (magnet === null) {
862
+ magnet = createMagnetGrid();
863
+ group.add(magnet.group);
864
+ }
865
+ classic.mesh.visible = false;
866
+ magnet.group.visible = true;
867
+ const fieldScale = magnetFieldScale(frame.camera.zoom, m.fadeZoom);
868
+ poleScratch.length = 0;
869
+ if (fieldScale > 0) {
870
+ for (const source of poles) poleScratch.push(...source.read(world));
871
+ }
872
+ const sourceCount = collectMagnetSources(
873
+ world,
874
+ frame,
875
+ m,
876
+ fieldScale,
877
+ poleScratch,
878
+ deps.readSpatial,
879
+ sourceScratch
880
+ );
881
+ const levels = collectMagnetLevels(frame, cfg);
882
+ magnet.update(frame, cfg, m, levels, sourceScratch, sourceCount);
883
+ },
884
+ configure(next) {
885
+ cfg = mergeGridConfig(cfg, next);
886
+ classic.configure(cfg);
887
+ },
888
+ dispose() {
889
+ classic.dispose();
890
+ magnet?.dispose();
891
+ magnet = null;
892
+ }
893
+ };
894
+ }
895
+
896
+ // ../ground/src/passes/guides-collect.ts
897
+ var guideQ = defineQuery([GuideLine]);
898
+ var barQ = defineQuery([SpacingBar]);
899
+ function collectGuides(world, frame, config = DEFAULT_SNAP_GUIDES_CONFIG) {
900
+ const soup = new SoupBuilder();
901
+ const cam = frame.camera;
902
+ const lw = config.lineWidth;
903
+ const guideColor = [config.color[0], config.color[1], config.color[2], config.guideAlpha];
904
+ const barColor = [config.color[0], config.color[1], config.color[2], config.spacingAlpha];
905
+ world.query(guideQ).each((b) => {
906
+ for (const r of b) {
907
+ const g = world.read(b.entity(r), GuideLine);
908
+ const fullSpan = g.from === g.to;
909
+ if (g.axis === "x") {
910
+ const sx = worldToScreen(g.at, 0, cam).x;
911
+ if (frame.width > 0 && (sx < -1 || sx > frame.width + 1)) continue;
912
+ const y0 = fullSpan ? 0 : worldToScreen(0, g.from, cam).y;
913
+ const y1 = fullSpan ? frame.height : worldToScreen(0, g.to, cam).y;
914
+ soup.rect(sx - lw / 2, Math.min(y0, y1), lw, Math.abs(y1 - y0), guideColor);
915
+ } else {
916
+ const sy = worldToScreen(0, g.at, cam).y;
917
+ if (frame.height > 0 && (sy < -1 || sy > frame.height + 1)) continue;
918
+ const x0 = fullSpan ? 0 : worldToScreen(g.from, 0, cam).x;
919
+ const x1 = fullSpan ? frame.width : worldToScreen(g.to, 0, cam).x;
920
+ soup.rect(Math.min(x0, x1), sy - lw / 2, Math.abs(x1 - x0), lw, guideColor);
921
+ }
922
+ }
923
+ });
924
+ world.query(barQ).each((b) => {
925
+ for (const r of b) {
926
+ const s = world.read(b.entity(r), SpacingBar);
927
+ const t = config.tickPx;
928
+ if (s.axis === "x") {
929
+ const a = worldToScreen(s.from, s.perp, cam);
930
+ const bp = worldToScreen(s.to, s.perp, cam);
931
+ if (frame.width > 0 && (Math.max(a.x, bp.x) < 0 || Math.min(a.x, bp.x) > frame.width)) continue;
932
+ soup.rect(Math.min(a.x, bp.x), a.y - lw / 2, Math.abs(bp.x - a.x), lw, barColor);
933
+ soup.rect(a.x - lw / 2, a.y - t, lw, t * 2, barColor);
934
+ soup.rect(bp.x - lw / 2, bp.y - t, lw, t * 2, barColor);
935
+ } else {
936
+ const a = worldToScreen(s.perp, s.from, cam);
937
+ const bp = worldToScreen(s.perp, s.to, cam);
938
+ if (frame.height > 0 && (Math.max(a.y, bp.y) < 0 || Math.min(a.y, bp.y) > frame.height)) continue;
939
+ soup.rect(a.x - lw / 2, Math.min(a.y, bp.y), lw, Math.abs(bp.y - a.y), barColor);
940
+ soup.rect(a.x - t, a.y - lw / 2, t * 2, lw, barColor);
941
+ soup.rect(bp.x - t, bp.y - lw / 2, t * 2, lw, barColor);
942
+ }
943
+ }
944
+ });
945
+ return soup.build();
946
+ }
947
+
948
+ // ../ground/src/passes/soup-mesh.ts
949
+ import { attribute } from "three/tsl";
950
+ import { BufferAttribute as BufferAttribute3, BufferGeometry as BufferGeometry2, DoubleSide, Mesh as Mesh3, MeshBasicNodeMaterial as MeshBasicNodeMaterial3 } from "three/webgpu";
951
+ function createSoupMesh(renderOrder) {
952
+ const material = new MeshBasicNodeMaterial3();
953
+ material.fragmentNode = attribute("color", "vec4");
954
+ material.transparent = true;
955
+ material.depthTest = false;
956
+ material.depthWrite = false;
957
+ material.side = DoubleSide;
958
+ let geometry = new BufferGeometry2();
959
+ let capacity = 0;
960
+ const mesh = new Mesh3(geometry, material);
961
+ mesh.frustumCulled = false;
962
+ mesh.renderOrder = renderOrder;
963
+ mesh.visible = false;
964
+ return {
965
+ mesh,
966
+ update(soup) {
967
+ if (soup.vertexCount === 0) {
968
+ mesh.visible = false;
969
+ geometry.setDrawRange(0, 0);
970
+ return;
971
+ }
972
+ if (soup.vertexCount > capacity) {
973
+ capacity = Math.max(soup.vertexCount, capacity * 2, 256);
974
+ const old = geometry;
975
+ geometry = new BufferGeometry2();
976
+ const pos = new Float32Array(capacity * 3);
977
+ const col = new Float32Array(capacity * 4);
978
+ pos.set(soup.positions);
979
+ col.set(soup.colors);
980
+ geometry.setAttribute("position", new BufferAttribute3(pos, 3));
981
+ geometry.setAttribute("color", new BufferAttribute3(col, 4));
982
+ mesh.geometry = geometry;
983
+ old.dispose();
984
+ } else {
985
+ const pos = geometry.getAttribute("position");
986
+ const col = geometry.getAttribute("color");
987
+ pos.array.set(soup.positions);
988
+ col.array.set(soup.colors);
989
+ pos.needsUpdate = true;
990
+ col.needsUpdate = true;
991
+ }
992
+ geometry.setDrawRange(0, soup.vertexCount);
993
+ mesh.visible = true;
994
+ },
995
+ dispose() {
996
+ geometry.dispose();
997
+ material.dispose();
998
+ }
999
+ };
1000
+ }
1001
+
1002
+ // ../ground/src/passes/guides.ts
1003
+ function createGuidesPass(initial = {}) {
1004
+ const config = { ...DEFAULT_SNAP_GUIDES_CONFIG, ...initial };
1005
+ const soup = createSoupMesh(2);
1006
+ return {
1007
+ name: "snap-guides",
1008
+ object: soup.mesh,
1009
+ arm(world, wake) {
1010
+ return [
1011
+ world.reactive.observeQuery(guideQ, wake, { cols: [GuideLine] }),
1012
+ world.reactive.observeQuery(barQ, wake, { cols: [SpacingBar] })
1013
+ ];
1014
+ },
1015
+ collect(world, frame) {
1016
+ soup.update(collectGuides(world, frame, config));
1017
+ },
1018
+ dispose() {
1019
+ soup.dispose();
1020
+ }
1021
+ };
1022
+ }
1023
+
575
1024
  // ../ground/src/passes/wires.ts
576
1025
  function createWiresPass(initial = {}, readPreview) {
577
1026
  const config = { ...DEFAULT_WIRES_CONFIG, ...initial };
@@ -682,12 +1131,53 @@ function createGroundRenderer(doc, opts = {}) {
682
1131
  };
683
1132
  }
684
1133
 
1134
+ // ../ground/src/poles.ts
1135
+ var localPointerQ = defineQuery([Pointer, PointerWorld, LocalPointer]);
1136
+ function localPointerPoles(opts = {}) {
1137
+ const strength = opts.strength ?? 1;
1138
+ return {
1139
+ read(world) {
1140
+ const out = [];
1141
+ world.query(localPointerQ).each((b) => {
1142
+ for (const r of b) {
1143
+ const p = world.read(b.entity(r), PointerWorld);
1144
+ out.push({ x: p.x, y: p.y, strength, space: "world" });
1145
+ }
1146
+ });
1147
+ return out;
1148
+ },
1149
+ subscribe(world, wake) {
1150
+ return world.reactive.observeQuery(localPointerQ, wake, { cols: [PointerWorld] });
1151
+ }
1152
+ };
1153
+ }
1154
+ var cursorVisualQ = defineQuery([Position, CursorVisual]);
1155
+ function cursorVisualPoles(opts = {}) {
1156
+ const strength = opts.strength ?? 1;
1157
+ return {
1158
+ read(world) {
1159
+ const out = [];
1160
+ world.query(cursorVisualQ).each((b) => {
1161
+ for (const r of b) {
1162
+ const p = world.read(b.entity(r), Position);
1163
+ out.push({ x: p.x, y: p.y, strength, space: "world" });
1164
+ }
1165
+ });
1166
+ return out;
1167
+ },
1168
+ subscribe(world, wake) {
1169
+ return world.reactive.observeQuery(cursorVisualQ, wake, { cols: [Position] });
1170
+ }
1171
+ };
1172
+ }
1173
+
685
1174
  // ../ground/src/index.ts
686
1175
  function ground(opts = {}) {
687
1176
  return (ctx) => {
688
1177
  const doc = ctx.host.container.ownerDocument;
689
1178
  const renderer = opts.rendererOverride ?? createGroundRenderer(doc, { forceWebGL: opts.forceWebGL === true });
690
- const grid = createGridPass(opts.grid ?? {});
1179
+ const poles = opts.poles === void 0 ? [] : Array.isArray(opts.poles) ? opts.poles : [opts.poles];
1180
+ const grid = createGridPass(opts.grid ?? {}, { poles, ...ctx.readSpatial !== void 0 ? { readSpatial: ctx.readSpatial } : {} });
691
1181
  const wires = createWiresPass(opts.wires ?? {}, ctx.readWirePreview);
692
1182
  const guides = createGuidesPass(opts.guides ?? {});
693
1183
  const passes = [grid, wires, guides, ...opts.passes ?? []];
@@ -704,12 +1194,20 @@ function ground(opts = {}) {
704
1194
  }
705
1195
  export {
706
1196
  DEFAULT_GRID_CONFIG,
1197
+ DEFAULT_GRID_MAGNET_CONFIG,
707
1198
  DEFAULT_SNAP_GUIDES_CONFIG,
708
1199
  DEFAULT_WIRES_CONFIG,
1200
+ MAX_MAGNET_SOURCES,
709
1201
  SoupBuilder,
710
1202
  collectGuides,
1203
+ collectMagnetLevels,
1204
+ collectMagnetSources,
711
1205
  collectWires,
1206
+ cursorVisualPoles,
712
1207
  ground,
713
- parseCssColor
1208
+ localPointerPoles,
1209
+ magnetFieldScale,
1210
+ parseCssColor,
1211
+ resolveMagnet
714
1212
  };
715
1213
  //# sourceMappingURL=ground.js.map