Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ When using Bend:
- `src/core/tailnet.bend` (pure: owner trust from `tailscale whois`, the owner's machines from `tailscale status`) and `src/app/link.bend` (a WebSocket client shaped like a hub, so the native window can switch to another machine's hub). Link effects (`src/app/effects/link.c` and `.js`) resolve hostnames and bound the handshake to five seconds; the window reconnects from the cached sequence and origin. `Accept.whois` bounds identity lookup too. Each hub finds the owner's other hubs (GET /hello) and lists them in the sidebar; `BACKPLANE_PEERS` names hubs off the tailnet.
- `src/core/diff.bend` (pure: unified diff → files, hunks, numbered lines; stats) and `src/server/git.bend` (IO: repo info, porcelain status, worktrees, hidden-ref checkpoints, stacked commit/push/PR). See `docs/git.md`. Native test: `bend test/native/git_test.bend -o build/git_test && build/git_test`.
- `src/core/hier.bend`: hierarchical schematics (pure: the sheets a file lists, paths resolved beside their parent, the tree as pre-order rows, where a row sits, its parent and children; a sheet that includes an ancestor is listed but never expanded; laws `hier_*`). main.bend's `Tree.load` reads every file the root reaches after each schematic read and sends the rows (`Sheets`); the window keeps them in the scratch (`@vw.tree`, `@vw.at`) and shows a trail and the sheet list (`Side.*` in layout.bend; actions `vw-sheet`, `vw-open`, `vw-up`). Test: `test/hier_test.bend`; headless: `test/native/sheet_frame.bend ROOT.kicad_sch`.
- The board viewer keeps its scene in (layer, key) order, which is paint order, so a frame is one pass (`V.Ops.scene`); items off the panel are culled before any geometry is transformed. The settled scene is drawn once per view into the frame's base by `src/gfx/fresh.bend` (`Fresh.frame`: the ops split into 64 px blocks, dealt over the cores, the image put together; the same image `R.Frame.render` draws), so a pan or zoom redraws it in parallel; the tiles compare a base only where their ops are the same (`Eq.tile`), and `Eq.image` stops at the first difference. Filled polygons are anti-aliased by distance to their nearest edge (`Cov.poly` in raster.bend). Bench: `test/native/viewer_bench.bend FILE [W H]` (pan, zoom and keys with the viewer open, each frame checked exact). Layers turn on and off per kind with a flip mask (`@vw.flip.<kind>`, `V.Look.on`; laws `look_flip_*`, `ops_hidden_draws_nothing`, `pick_hidden_skipped`). Bench: `test/native/board_frame.bend BOARD.kicad_pcb`.
- The board viewer keeps its scene in (layer, key) order, which is paint order, so a frame is one pass (`V.Ops.scene`); items off the panel are culled before any geometry is transformed. The settled scene is drawn once per view into the frame's base by `src/gfx/fresh.bend` (`Fresh.frame`: the ops split into 64 px blocks, dealt over the cores, the image put together; the same image `R.Frame.render` draws), so a pan or zoom redraws it in parallel; the tiles compare a base only where their ops are the same (`Eq.tile`), and `Eq.image` stops at the first difference. Filled polygons are anti-aliased by distance to their nearest edge (`Cov.poly` in raster.bend). Bench: `test/native/viewer_bench.bend FILE [W H]` (pan, zoom and keys with the viewer open, each frame checked exact). Layers turn on and off per kind with a flip mask (`@vw.flip.<kind>`, `V.Look.on`; laws `look_flip_*`, `ops_hidden_draws_nothing`, `pick_hidden_skipped`). Every client lists a board's layers in KiCad's order with KiCad's names, and has KiCad's high contrast: a click on a layer's name makes it the active layer and dims the rest, a click on it again puts all back; the list's All/Dim/Only (the window's ctrl+H cycles them) says how the others show; the active layer is drawn last, on top, and keeps the board's outline and, for copper, its pads, vias and holes (`src/core/layers.bend`'s `Lyr.*`: order, names, `Focus`, `Lyr.shade`, `Lyr.masks`, `Lyr.pick`/`set`/`cycle`; laws `lyr_*`; test `test/layers_test.bend`). The focus is kept per kind beside the flips (window `@vw.focus.<kind>`, web `@focus.<kind>`, phones `@view.focus.<kind>`); the window carries it in the kind word's high bits into `V.Look.on` (`Lyr.kind.with`, main.bend's `fk`), the web and phones get the hidden and dimmed layers as bit masks and the layer to draw last (`data-off`/`data-dim`/`data-lift`, the phones' `off`/`dim`/`lift`). Actions `vw-layer-pick`/`vw-layer-mode` (window), `view-layer-pick`/`view-layer-mode` (web and phones). Bench: `test/native/board_frame.bend BOARD.kicad_pcb`.
- Bots in the window: `src/app/bots.bend` (`Bv`: the sidebar's cats and rooms under the projects, a bot's header and tabs, rooms, remote bots; `Bv.frame` wraps `Lay.frame.x`, whose hooks add to the sidebar and replace the main area), `src/app/space.bend` (`Spv`: a bot's space blocks drawn natively), `src/app/forms.bend` (`Fm`: clipped chips and form fields whose text is the client's `@f.<name>` scratch; nui.bend's `Fld` edits them, focus id `Fld.id(name, enter action, value prefix)`), `src/app/cats.bend` (`Cats`: core/cat.bend's cats flattened once per look+mood into a cache kept in main.bend's `Kept`, posed per frame). Cat frames: 33 ms timer (main.bend's `Shell.cats`) only while `Bv.moving` (a moving mood, window focused: `@blur`, no lightbox). A bot's browser frames load as images keyed `shot:<bot>:<n>` (main.bend's `Shot`). Screens without a window: `build/bots_snap DIR` (`test/native/bots_snap.bend`).
- The owner's machines pair and sync by themselves (client.bend's `Mate.*`: the lower address asks `POST /bots/pair`, owner-trusted, and joins the invite it answers; `--no-tailscale` hubs pair only with `BACKPLANE_PEERS`); Settings' "Your machines" (`Mate.rows`, all four clients) shows each and turns one off (`sync.<name>`: `Far.own`, `Side.projs`). A pull carries the owner's catalog (`Fr.catalog`, `Fh.lasts`, `Fh.cat`), so a client lists a machine's projects at once and marks each "(syncing)", faded, until the link's head passes its last number (`Fr.syncing`, `Label.syncing`). `>machine:slug` names a thread elsewhere (refs.bend's `Refs.x.*`); `src/core/xref.bend` resolves it from the hub's mirror for the agent and answers `thread_read` on it. Laws `mate_*`, `far_catalog_*`, `refs_machine_word`; tests `test/far_test.bend`, `test/refs_test.bend`, `bun test/tools/far_e2e.ts`. See docs/bots.md "Threads elsewhere".
- `src/core/fview.bend` and `src/server/farview.bend`: the viewers for a thread elsewhere (pure: the read-only requests the owning hub answers, safe paths, the cache folder `<home>/farfs/<link>/<owner's folder>`, reply fixes; IO: the owner's `view.ver`/`view.get` and the client's hub bringing files into the cache; client.bend's `Fv.*` asks with `far.view`, all four clients; laws `fview_*`; test `test/fview_test.bend`, `bun test/tools/viewers_far_e2e.ts`). See docs/bots.md "Viewers elsewhere".
Expand Down
51 changes: 51 additions & 0 deletions LAWS.bend
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import ./src/core/live.bend as LV
import ./src/core/farlive.bend as FLV
import ./src/core/contain.bend as CTN
import ./src/core/qr.bend as QR
import ./src/core/layers.bend as LY

# Backplane's rules, stated over the real code. `bend PROOF.bend` proves each
# one. Humans own this file: an agent may add laws, never weaken or delete
Expand Down Expand Up @@ -6344,3 +6345,53 @@ law rview_enc_breaks:

law rview_spec_local:
{RVW.Rview.spec("", "/c", "t", "g") == "" : String}

# The layer list and its focus (core/layers.bend)
# ------------------------------------------------
# A board's layers listed in KiCad's order: copper front to back, pads,
# vias, holes, then the technical layers front/back paired
law lyr_board_order_kicad:
{LY.Lyr.order(0) == [12, 11, 10, 9, 8, 7, 6, 5, 13, 23, 24, 15, 2, 16, 3, 14, 4, 19, 20, 22, 21, 17, 1, 18, 0] : List<&2, U32>}

# with no focus every layer looks as it does
law lyr_plain_as_is:
{LY.Lyr.shade(0, LY.Lyr.plain(), 16, (7, 200)) == (7, 200) : U32 & U32}

# dimmed: the other layers at a quarter
law lyr_dim_quarter:
{LY.Lyr.shade(0, LY.Focus{12, 1}, 16, (7, 200)) == (7, 50) : U32 & U32}

# only: the other layers hidden
law lyr_only_hides:
{LY.Lyr.shade(0, LY.Focus{12, 2}, 16, (7, 200)) == (7, 0) : U32 & U32}

# the active layer always shows, even one hidden by default
law lyr_active_shows:
{LY.Lyr.shade(0, LY.Focus{15, 2}, 15, (7, 0)) == (7, 200) : U32 & U32}

# a copper layer keeps its vias, and every layer the board's outline
law lyr_copper_keeps_vias:
{LY.Lyr.shade(0, LY.Focus{5, 2}, 23, (7, 255)) == (7, 255) : U32 & U32}

law lyr_outline_stays:
{LY.Lyr.shade(0, LY.Focus{16, 2}, 22, (7, 255)) == (7, 255) : U32 & U32}

# a click on the active layer again puts every layer back
law lyr_pick_again_all:
{LY.Lyr.pick(LY.Focus{16, 1}, 16) == LY.Focus{16, 0} : LY.Focus}

# ctrl+H goes around: dimmed, hidden, as they are
law lyr_cycle_round:
{LY.Lyr.cycle(0, LY.Lyr.cycle(0, LY.Lyr.cycle(0, LY.Focus{12, 0}))) == LY.Focus{12, 0} : LY.Focus}

# the window's kind word carries the focus and gives the kind back
law lyr_kind_round:
{LY.Lyr.kind.focus(LY.Lyr.kind.with(9, LY.Focus{16, 2})) == LY.Focus{16, 2} : LY.Focus}

law lyr_kind_base:
{LY.Lyr.kind.base(LY.Lyr.kind.with(9, LY.Focus{16, 2})) == 9 : U32}

# the masks the web and phones draw from agree: only hides every layer
# not kept and never the active one
law lyr_masks_only:
{LY.Masks.off(LY.Lyr.masks(0, 4096, LY.Focus{12, 2})) == 37736447 : U32}
36 changes: 36 additions & 0 deletions PROOF.bend
Original file line number Diff line number Diff line change
Expand Up @@ -7968,3 +7968,39 @@ def Laws.rview_root_far_climbs():

def Laws.rview_root_own_ignores_claim():
{==}

def Laws.lyr_board_order_kicad():
{==}

def Laws.lyr_plain_as_is():
{==}

def Laws.lyr_dim_quarter():
{==}

def Laws.lyr_only_hides():
{==}

def Laws.lyr_active_shows():
{==}

def Laws.lyr_copper_keeps_vias():
{==}

def Laws.lyr_outline_stays():
{==}

def Laws.lyr_pick_again_all():
{==}

def Laws.lyr_cycle_round():
{==}

def Laws.lyr_kind_round():
{==}

def Laws.lyr_kind_base():
{==}

def Laws.lyr_masks_only():
{==}
12 changes: 10 additions & 2 deletions mobile/android/app/src/main/java/dev/backplane/mobile/PlotView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,11 @@ class PlotRenderer : GLSurfaceView.Renderer {
// the layers the user turned off, a bit each
@Volatile var off = 0
private fun shown(layer: Int) = layer !in 0..31 || (off shr layer) and 1 == 0
// the layer focus (core/layers.bend): layers drawn at a quarter, a bit
// each, and the active one, drawn after the rest (255 none)
@Volatile var dim = 0
@Volatile var lift = 255
private fun dimmed(layer: Int) = layer in 0..31 && (dim shr layer) and 1 == 1
@Volatile var three = false
@Volatile var orbit = Orbit()
@Volatile var thick = 1600f
Expand Down Expand Up @@ -532,7 +537,8 @@ class PlotRenderer : GLSurfaceView.Renderer {
}

private fun draw(l: Layer) =
layer(l.color, bufs[0], listOf(l.caps to 1f, l.freshCaps to fade), bufs[1], listOf(l.tris to 1f, l.freshTris to fade))
layer(if (dimmed(l.layer)) l.color.copyOf().also { it[3] *= 0.25f } else l.color,
bufs[0], listOf(l.caps to 1f, l.freshCaps to fade), bufs[1], listOf(l.tris to 1f, l.freshTris to fade))

private fun hi() {
if (hiCaps + hiTris == 0) return
Expand Down Expand Up @@ -600,7 +606,9 @@ class PlotRenderer : GLSurfaceView.Renderer {
if (hiLayer in face) hi()
}
} else {
for (l in layers) if (shown(l.layer)) draw(l)
// the active layer on top, as KiCad draws it
for (l in layers) if (shown(l.layer) && l.layer != lift) draw(l)
for (l in layers) if (shown(l.layer) && l.layer == lift) draw(l)
hi()
}
// the scene to the screen
Expand Down
10 changes: 8 additions & 2 deletions mobile/android/app/src/main/java/dev/backplane/mobile/Screen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ data class Viewer(
val hist: HistBar? = null,
// the Files tab (open "files"), and an image or a PDF it opened
val files: FilesPage? = null, val file: FileView? = null,
// the layer focus (core/layers.bend): the layers drawn dimmed (a bit
// each), the one drawn last (255 none), the list's All/Dim/Only (0-2)
// and whether it has one (a board), and the Layers button's words
val dim: Int = 0, val lift: Int = 255, val layerMode: Int = 0, val layerModes: Boolean = false, val layerHead: String = "Layers",
)

// the Files tab: where the folder is, its rows (a tap sends action with
Expand Down Expand Up @@ -218,7 +222,7 @@ data class MechShot(val view: String, val url: String)

data class Sheet(val label: String, val value: String, val on: Boolean, val loop: Boolean)

data class LayerRow(val layer: Int, val name: String, val on: Boolean)
data class LayerRow(val layer: Int, val name: String, val on: Boolean, val active: Boolean = false)

// the composer's model chip: its label, the models ("model" sends one)
// and the efforts the current one takes ("effort")
Expand Down Expand Up @@ -493,7 +497,7 @@ private fun viewer(o: JSONObject) = Viewer(
},
o.optBoolean("light"), ints(o.optJSONArray("look")),
o.optJSONArray("sheets").map { Sheet(it.optString("label"), it.optString("value"), it.optBoolean("on"), it.optBoolean("loop")) },
o.optJSONArray("layerList").map { LayerRow(it.optInt("layer"), it.optString("name"), it.optBoolean("on")) },
o.optJSONArray("layerList").map { LayerRow(it.optInt("layer"), it.optString("name"), it.optBoolean("on"), it.optBoolean("active")) },
o.optInt("off"), o.optBoolean("parts", true), o.optString("note"),
o.optJSONObject("mech")?.let { p ->
MechPage(p.optString("say"), p.optString("note"), choices(p.optJSONArray("parts")), p.optString("name"), p.optString("path"),
Expand All @@ -513,6 +517,8 @@ private fun viewer(o: JSONObject) = Viewer(
f.optBoolean("ready"))
},
file = o.optJSONObject("file")?.let { FileView(it.optString("name"), it.optString("kind"), it.optString("url")) },
dim = o.optInt("dim"), lift = o.optInt("lift", 255), layerMode = o.optInt("layerMode"), layerModes = o.optBoolean("layerModes"),
layerHead = o.optString("layerHead", "Layers"),
)

private fun strs(a: JSONArray?): List<String> =
Expand Down
23 changes: 17 additions & 6 deletions mobile/android/app/src/main/java/dev/backplane/mobile/Ui.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1039,7 +1039,9 @@ private fun PlotScreen(m: AppModel, v: Viewer) {
s.renderer.orbit.fov = v.fov
s.setThree(v.open == "3d" || v.open == "mech")
if (f != null && f.none.isEmpty()) s.show(f, v.bg, v.slab, v.look)
if (s.renderer.off != v.off) { s.renderer.off = v.off; s.requestRender() }
if (s.renderer.off != v.off || s.renderer.dim != v.dim || s.renderer.lift != v.lift) {
s.renderer.off = v.off; s.renderer.dim = v.dim; s.renderer.lift = v.lift; s.requestRender()
}
if (v.open == "3d" || v.open == "mech") s.mesh(mesh)
s.mark(v.picked)
})
Expand Down Expand Up @@ -1285,13 +1287,22 @@ private fun ViewerControls(m: AppModel, v: Viewer, present: Set<Int>, modifier:
var open by remember { mutableStateOf(false) }
OutlinedButton(onClick = { open = true }) {
Icon(Icons.Filled.Layers, null, tint = fg)
Text("Layers", color = fg, modifier = Modifier.padding(start = 6.dp))
Text(v.layerHead, color = fg, maxLines = 1, modifier = Modifier.padding(start = 6.dp))
}
// stays open: several layers are turned on and off in a row
// stays open: several layers are turned on and off in a row. A
// row's box shows or hides it; its name makes it the active layer
// (again: all back), and over a board's rows All, Dim, Only say how
// the others show (core/layers.bend)
DropdownMenu(open, { open = false }) {
for (l in layers) DropdownMenuItem(text = { Text(l.name) },
leadingIcon = { Checkbox(l.on, null) },
onClick = { m.act("view-layer", l.layer.toString()) })
if (v.layerModes) Row(Modifier.padding(horizontal = 12.dp, vertical = 4.dp)) {
for ((i, label) in listOf("All", "Dim", "Only").withIndex())
FilterChip(v.layerMode == i, { m.act("view-layer-mode", i.toString()) }, label = { Text(label) },
modifier = Modifier.padding(end = 6.dp))
}
for (l in layers) DropdownMenuItem(
text = { Text(l.name, color = if (l.active) MaterialTheme.colorScheme.primary else Color.Unspecified) },
leadingIcon = { Checkbox(l.on, { m.act("view-layer", l.layer.toString()) }) },
onClick = { m.act("view-layer-pick", l.layer.toString()) })
}
}
if (v.open == "3d" && v.layers.isNotEmpty()) FilterChip(v.parts, { m.act("view-parts") }, label = { Text("Parts", color = fg) },
Expand Down
Loading
Loading