diff --git a/AGENTS.md b/AGENTS.md index 05964e47ad..63d5746f38 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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.`, `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.`, `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.`, web `@focus.`, phones `@view.focus.`); 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.` 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::` (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.`: `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 `/farfs//`, 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". diff --git a/LAWS.bend b/LAWS.bend index 68c83412d5..774c7a64b7 100644 --- a/LAWS.bend +++ b/LAWS.bend @@ -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 @@ -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} diff --git a/PROOF.bend b/PROOF.bend index f4c3fd6614..73333b0d26 100644 --- a/PROOF.bend +++ b/PROOF.bend @@ -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(): + {==} diff --git a/mobile/android/app/src/main/java/dev/backplane/mobile/PlotView.kt b/mobile/android/app/src/main/java/dev/backplane/mobile/PlotView.kt index 1b59c04c06..d228887b85 100644 --- a/mobile/android/app/src/main/java/dev/backplane/mobile/PlotView.kt +++ b/mobile/android/app/src/main/java/dev/backplane/mobile/PlotView.kt @@ -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 @@ -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 @@ -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 diff --git a/mobile/android/app/src/main/java/dev/backplane/mobile/Screen.kt b/mobile/android/app/src/main/java/dev/backplane/mobile/Screen.kt index fa86efffac..fec9e64ac1 100644 --- a/mobile/android/app/src/main/java/dev/backplane/mobile/Screen.kt +++ b/mobile/android/app/src/main/java/dev/backplane/mobile/Screen.kt @@ -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 @@ -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") @@ -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"), @@ -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 = diff --git a/mobile/android/app/src/main/java/dev/backplane/mobile/Ui.kt b/mobile/android/app/src/main/java/dev/backplane/mobile/Ui.kt index 72ffc86a89..5ad43e9bfb 100644 --- a/mobile/android/app/src/main/java/dev/backplane/mobile/Ui.kt +++ b/mobile/android/app/src/main/java/dev/backplane/mobile/Ui.kt @@ -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) }) @@ -1285,13 +1287,22 @@ private fun ViewerControls(m: AppModel, v: Viewer, present: Set, 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) }, diff --git a/mobile/ios/Backplane/PlotView.swift b/mobile/ios/Backplane/PlotView.swift index 39a1b18e2e..dd35712ad0 100644 --- a/mobile/ios/Backplane/PlotView.swift +++ b/mobile/ios/Backplane/PlotView.swift @@ -136,6 +136,16 @@ final class PlotRenderer: NSObject, MTKViewDelegate { // the layers the user turned off, a bit each var hiddenLayers: UInt32 = 0 func shown(_ layer: Int) -> Bool { layer < 0 || layer > 31 || (hiddenLayers >> UInt32(layer)) & 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) + var dimLayers: UInt32 = 0 + var liftLayer = 255 + func dimmed(_ layer: Int) -> Bool { layer >= 0 && layer <= 31 && (dimLayers >> UInt32(layer)) & 1 == 1 } + private func tint(_ l: LayerDraw) -> SIMD4 { + var c = l.color + if dimmed(l.layer) { c.w *= 0.25 } + return c + } // 2D view: pixels per micrometre and where 0,0 lands, in points var scale: Float = 0.01 var off = SIMD2(0, 0) @@ -474,9 +484,14 @@ final class PlotRenderer: NSObject, MTKViewDelegate { } } else { frame(cb, t, clear: true, depth: false) { _ in } - for l in layers where shown(l.layer) { + // the active layer on top, as KiCad draws it + for l in layers where shown(l.layer) && l.layer != liftLayer { + let (c, tr) = draws(l) + layer(cb, t, &u, color: tint(l), caps: c, tris: tr) + } + for l in layers where shown(l.layer) && l.layer == liftLayer { let (c, tr) = draws(l) - layer(cb, t, &u, color: l.color, caps: c, tris: tr) + layer(cb, t, &u, color: tint(l), caps: c, tris: tr) } hi(cb, t, &u) } @@ -804,8 +819,10 @@ struct PlotCanvasView: UIViewRepresentable { c.renderer.three = three c.refit() } - if c.renderer.hiddenLayers != (viewer.off ?? 0) { + if c.renderer.hiddenLayers != (viewer.off ?? 0) || c.renderer.dimLayers != (viewer.dim ?? 0) || c.renderer.liftLayer != (viewer.lift ?? 255) { c.renderer.hiddenLayers = viewer.off ?? 0 + c.renderer.dimLayers = viewer.dim ?? 0 + c.renderer.liftLayer = viewer.lift ?? 255 c.setNeedsDisplay() } if let f = frame, f.at != context.coordinator.shown || (viewer.look ?? []) != context.coordinator.look { @@ -899,15 +916,32 @@ struct ViewerControls: View { .buttonStyle(.bordered) } if !layers.isEmpty { - // stays open: several layers are turned on and off in a row + // stays open: several layers are turned on and off in a row. + // Over a board's rows, how the layers other than the active one + // show (All, Dim, Only); each row shows or hides its layer, and + // its "stand out" button makes it the active layer (again: all + // back) (core/layers.bend) Menu { + if viewer.layerModes ?? false { + Picker("Others", selection: Binding(get: { viewer.layerMode ?? 0 }, set: { model.act("view-layer-mode", String($0)) })) { + Text("All").tag(0) + Text("Dim").tag(1) + Text("Only").tag(2) + } + .pickerStyle(.segmented) + } ForEach(layers, id: \.layer) { l in - Button { model.act("view-layer", String(l.layer)) } label: { - Label(l.name, systemImage: l.on ? "checkmark.square" : "square") + ControlGroup { + Button { model.act("view-layer", String(l.layer)) } label: { + Label(l.name, systemImage: l.on ? "checkmark.square" : "square") + } + Button { model.act("view-layer-pick", String(l.layer)) } label: { + Label(l.active ?? false ? "All back" : "Stand out", systemImage: l.active ?? false ? "scope" : "circle.dotted") + } } } } label: { - Label("Layers", systemImage: "square.3.layers.3d") + Label(viewer.layerHead ?? "Layers", systemImage: "square.3.layers.3d") } .buttonStyle(.bordered) .menuActionDismissBehavior(.disabled) diff --git a/mobile/ios/Backplane/Screen.swift b/mobile/ios/Backplane/Screen.swift index fee9108c12..d2877f015e 100644 --- a/mobile/ios/Backplane/Screen.swift +++ b/mobile/ios/Backplane/Screen.swift @@ -254,6 +254,14 @@ struct Viewer: Decodable { let sheets: [SheetRow]? let layerList: [LayerRow]? let off: UInt32? + // 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 + let dim: UInt32? + let lift: Int? + let layerMode: Int? + let layerModes: Bool? + let layerHead: String? let parts: Bool? // what the hub says about the source (parts with no 3D model) let note: String? @@ -345,6 +353,8 @@ struct LayerRow: Decodable, Hashable { let layer: Int let name: String let on: Bool + // the active layer of the focus (core/layers.bend) + let active: Bool? } // the composer's model chip: its label, the models ("model" sends one) diff --git a/src/app/layout.bend b/src/app/layout.bend index b9c040d546..89352947f5 100644 --- a/src/app/layout.bend +++ b/src/app/layout.bend @@ -33,6 +33,7 @@ import ../core/voice.bend as VC import ../core/mech.bend as MC import ../core/hist.bend as HS import ../core/look.bend as LO +import ../core/layers.bend as LY import ../core/tools.bend as TO import ../core/emb.bend as EM import ../core/onboard.bend as OB @@ -3410,10 +3411,6 @@ def Lay.viewer.vw(+ty: Ty, v: V.Vw, +draft: String, +diff: String, +mech: Bool, # for a schematic with sheets, the trail from the root to the sheet on # show and the whole sheet tree in the same list (a click goes there). -# (and a comparison's marks, listed when a comparison shows) -def Side.names(+k: U32) -> List<&2, String>: - LO.Look.names(U32.or(k, LO.Look.diff()), Bool.pick(List<&2, String>, U32.is_eq(k, 1), SC.Sch.layer_names(), B.Layer.names())) - def Side.color(l: U32 & U32) -> U32: (c, _) = l c @@ -3421,34 +3418,59 @@ def Side.color(l: U32 & U32) -> U32: def Side.has(+present: U32, +i: U32, +count: Nat) -> Bool: Bool.and(Nat.is_lt(U32.to_nat(i), count), Bool.not(U32.is_eq(U32.and(present, V.Look.bit(i)), 0))) -def Side.layer(+ty: Ty, +k: U32, +l: U32, +name: String, +flip: U32, +x: U32, +y: U32, +w: U32, +c: D.Clip) -> Out: +# a layer's row: its colour (a click shows or hides it, vw-layer) and its +# name (a click makes it the active layer, the rest dimmed, vw-layer-pick; +# core/layers.bend); the active one lit +def Side.layer(+ty: Ty, +k: U32, +l: U32, +flip: U32, +act: Bool, +x: U32, +y: U32, +w: U32, +c: D.Clip) -> Out: +on = V.Look.shows(k, l, flip) - Out{List.append(&2, R.Op, [D.Draw.rect(c, (x + 12 : U32), (y + 6 : U32), 10, 10, Side.color(V.Look.on(k, l, flip)), Bool.pick(U32, on, 255, 50))], - Ty.text(ty, 2n, name, c, (x + 32 : U32), y, 22, Bool.pick(U32, on, T.Th.text(), T.Th.faint()))), - [Lay.hit(x, y, w, 22, "vw-layer", U32.show(l))]} + Out{List.concat(&2, R.Op, [Bool.pick(List<&2, R.Op>, act, [D.Draw.rect(c, (x + 4 : U32), y, (w - 8 : U32), 22, T.Th.sel(), 255)], Nil{}), + [D.Draw.rect(c, (x + 12 : U32), (y + 6 : U32), 10, 10, Side.color(V.Look.on(k, l, flip)), Bool.pick(U32, on, 255, 50))], + Ty.text(ty, 2n, LY.Lyr.name(k, l), c, (x + 32 : U32), y, 22, Bool.pick(U32, act, T.Th.accent(), Bool.pick(U32, on, T.Th.text(), T.Th.faint())))]), + [Lay.hit(x, y, 28, 22, "vw-layer", U32.show(l)), Lay.hit((x + 28 : U32), y, (w - 28 : U32), 22, "vw-layer-pick", U32.show(l))]} -def Side.layer.if(show: Bool, +ty: Ty, +k: U32, +l: U32, +name: String, +flip: U32, +x: U32, +y: U32, +w: U32, +c: D.Clip) -> Out: +def Side.layer.if(show: Bool, +ty: Ty, +k: U32, +l: U32, +flip: U32, +act: Bool, +x: U32, +y: U32, +w: U32, +c: D.Clip) -> Out: match show: case True{}: - Side.layer(ty, k, l, name, flip, x, y, w, c) + Side.layer(ty, k, l, flip, act, x, y, w, c) case False{}: Out.none() -# one row per layer the scene has, from y down -def Side.layers(ns: List<&2, String>, +i: U32, +y: U32, +ty: Ty, +k: U32, +present: U32, +flip: U32, +x: U32, +w: U32, +c: D.Clip) -> List<&2, Out>: - match ns: +# one row per layer the scene has, in KiCad's order (LY.Lyr.order), from y down +def Side.layers(ls: List<&2, U32>, +y: U32, +ty: Ty, +k: U32, +present: U32, +flip: U32, +f: LY.Focus, +x: U32, +w: U32, +c: D.Clip) -> List<&2, Out>: + match ls: case Nil{}: Nil{} - case +nm <> t: - Side.layer.if(Side.has(present, i, V.Look.count(k)), ty, k, i, nm, flip, x, y, w, c) - <> Side.layers(t, (i + 1 : U32), Bool.pick(U32, Side.has(present, i, V.Look.count(k)), (y + 22 : U32), y), ty, k, present, flip, x, w, c) + case +i <> t: + Side.layer.if(Side.has(present, i, V.Look.count(k)), ty, k, i, flip, Bool.and(LY.Lyr.on(f), U32.is_eq(i, LY.Lyr.active(f))), x, y, w, c) + <> Side.layers(t, Bool.pick(U32, Side.has(present, i, V.Look.count(k)), (y + 22 : U32), y), ty, k, present, flip, f, x, w, c) -def Side.n(ns: List<&2, String>, +i: U32, +present: U32, +count: Nat, +acc: U32) -> U32: - match ns: +def Side.n(ls: List<&2, U32>, +present: U32, +count: Nat, +acc: U32) -> U32: + match ls: case Nil{}: acc - case _ <> t: - Side.n(t, (i + 1 : U32), present, count, Bool.pick(U32, Side.has(present, i, count), (acc + 1 : U32), acc)) + case +i <> t: + Side.n(t, present, count, Bool.pick(U32, Side.has(present, i, count), (acc + 1 : U32), acc)) + +# the order the list follows (a comparison's marks after the kind's own) +def Side.order(+k: U32) -> List<&2, U32>: + LY.Lyr.order(U32.or(k, LO.Look.diff())) + +# KiCad's high contrast over a board's list: the layers other than the +# active one as they are, dimmed or hidden (vw-layer-mode) +def Side.mode(+ty: Ty, +m: U32, +cur: U32, +label: String, +x: U32, +y: U32, +w: U32, +c: D.Clip) -> Out: + Out{List.append(&2, R.Op, Lay.box(c, x, y, w, 24, 4, Bool.pick(U32, U32.is_eq(m, cur), T.Th.sel(), T.Th.raise()), Bool.pick(U32, U32.is_eq(m, cur), T.Th.accent(), T.Th.line())), + Ty.text(ty, 2n, label, c, (x + (w - Ty.width(ty, 2n, label)) / 2 : U32), y, 24, Bool.pick(U32, U32.is_eq(m, cur), T.Th.text(), T.Th.dim()))), + [Lay.hit(x, y, w, 24, "vw-layer-mode", U32.show(m))]} + +def Side.modes(board: Bool, +ty: Ty, +f: LY.Focus, +x: U32, +y: U32, +w: U32, +c: D.Clip) -> Out: + match board: + case True{}: + +cur = {Bool.pick(U32, LY.Lyr.on(f), LY.Lyr.mode(f), 0) : U32} + +bw = {((w - 24) / 3 : U32) : U32} + Out.all([Side.mode(ty, 0, cur, "All", (x + 12 : U32), y, bw, c), Side.mode(ty, 1, cur, "Dim", (x + 12 + bw : U32), y, bw, c), + Side.mode(ty, 2, cur, "Only", (x + 12 + bw * 2 : U32), y, bw, c)]) + case False{}: + Out.none() def Side.sheet(+ty: Ty, r: HI.Row, +j: Nat, +at: Nat, +x: U32, +y: U32, +w: U32, +c: D.Clip) -> Out: match r: @@ -3469,27 +3491,32 @@ def Side.sheets(rs: List<&2, HI.Row>, +j: Nat, +at: Nat, +ty: Ty, +x: U32, +y: U def Side.reset(+ty: Ty, +x: U32, +y: U32, +c: D.Clip) -> Out: Out{Ty.text(ty, 2n, "Reset layers", c, (x + 12 : U32), y, 22, T.Th.accent()), [Lay.hit(x, y, 200, 22, "vw-layers-reset", "")]} -# the open list: the sheet tree (schematics with sheets), then the layers -def Side.list(+ty: Ty, +k: U32, +present: U32, +flip: U32, +rs: List<&2, HI.Row>, +at: Nat, +x1: U32, +h: U32) -> Out: +# the open list: the sheet tree (schematics with sheets), then a board's +# All/Dim/Only, then the layers +def Side.list(+ty: Ty, +k: U32, +present: U32, +flip: U32, +f: LY.Focus, +rs: List<&2, HI.Row>, +at: Nat, +x1: U32, +h: U32) -> Out: +w = {240 : U32} +x = {(x1 - 16 - w : U32) : U32} +ns = {U32.from_nat(Bool.pick(Nat, Nat.is_gt(List.length(&2, HI.Row, rs), 1n), List.length(&2, HI.Row, rs), 0n)) : U32} +sh = {Bool.pick(U32, U32.is_eq(ns, 0), 0, (ns * 22 + 9 : U32)) : U32} - +nl = {Side.n(Side.names(k), 0, present, V.Look.count(k), 0) : U32} - +rh = {Bool.pick(U32, U32.is_eq(flip, 0), 0, 22) : U32} - +ph = {U32.min((sh + nl * 22 + rh + 12 : U32), (h - 112 : U32)) : U32} + +board = U32.is_eq(k, 0) + +mh = {Bool.pick(U32, board, 32, 0) : U32} + +nl = {Side.n(Side.order(k), present, V.Look.count(k), 0) : U32} + +reset = Bool.or(Bool.not(U32.is_eq(U32.and(flip, 536870911), 0)), LY.Lyr.on(f)) + +rh = {Bool.pick(U32, reset, 22, 0) : U32} + +ph = {U32.min((sh + mh + nl * 22 + rh + 12 : U32), (h - 112 : U32)) : U32} +c = {D.Clip{x, 96, (x + w : U32), (96 + ph : U32)} : D.Clip} Out.all([ Out{Lay.box(D.Clip.all(), x, 96, w, ph, 6, T.Th.panel(), T.Th.line()), [Lay.hit(x, 96, w, ph, "vw-panel", "")]}, Out.all(Side.sheets(Bool.pick(List<&2, HI.Row>, U32.is_eq(ns, 0), Nil{}, rs), 0n, at, ty, x, 102, w, c)), Out.ops(Bool.pick(List<&2, R.Op>, U32.is_eq(ns, 0), Nil{}, [D.Draw.rect(c, (x + 8 : U32), (102 + sh - 5 : U32), (w - 16 : U32), 1, T.Th.line(), 255)])), - Out.all(Side.layers(Side.names(k), 0, (102 + sh : U32), ty, k, present, flip, x, w, c)), - Out.when(Bool.not(U32.is_eq(flip, 0)), Side.reset(ty, x, (102 + sh + nl * 22 : U32), c))]) + Side.modes(board, ty, f, x, (102 + sh : U32), w, c), + Out.all(Side.layers(Side.order(k), (102 + sh + mh : U32), ty, k, present, flip, f, x, w, c)), + Out.when(reset, Side.reset(ty, x, (102 + sh + mh + nl * 22 : U32), c))]) -def Side.list.if(open: Bool, +ty: Ty, +k: U32, +ss: List<&2, V.Seen>, +flip: U32, +rs: List<&2, HI.Row>, +at: Nat, +x1: U32, +h: U32) -> Out: +def Side.list.if(open: Bool, +ty: Ty, +k: U32, +ss: List<&2, V.Seen>, +flip: U32, +f: LY.Focus, +rs: List<&2, HI.Row>, +at: Nat, +x1: U32, +h: U32) -> Out: match open: case True{}: - Side.list(ty, k, V.Layers.present(ss, 0), flip, rs, at, x1, h) + Side.list(ty, k, V.Layers.present(ss, 0), flip, f, rs, at, x1, h) case False{}: Out.none() @@ -3515,6 +3542,10 @@ def Side.ground.if(on: Bool, +ty: Ty, +n: S.Nui, +x1: U32) -> Out: case False{}: Out.none() +# the Layers chip names the active layer while one stands out +def Side.head(+k: U32, +f: LY.Focus) -> String: + Bool.pick(String, LY.Lyr.on(f), "Layers · " ++ LY.Lyr.name(k, LY.Lyr.active(f)), "Layers") + def Side.k(board: Bool, +ty: Ty, +n: S.Nui, +k: U32, +ss: List<&2, V.Seen>, +x0: U32, +x1: U32, +h: U32) -> Out: match board: case True{}: @@ -3524,9 +3555,9 @@ def Side.k(board: Bool, +ty: Ty, +n: S.Nui, +k: U32, +ss: List<&2, V.Seen>, +x0: +sheets = Bool.and(U32.is_eq(k, 1), Nat.is_gt(List.length(&2, HI.Row, rs), 1n)) Out.all([ Out.all(Side.trail(Bool.pick(List<&2, Nat>, sheets, HI.Hier.chain(rs, at), Nil{}), rs, at, ty, (x0 + 16 : U32))), - Lay.chip(ty, (x1 - 16 - Lay.button.w(ty, "Layers") : U32), 60, "Layers", open, "vw-layers", ""), - Side.ground(ty, n, (x1 - 22 - Lay.button.w(ty, "Layers") - 28 : U32)), - Side.list.if(open, ty, k, ss, S.Nui.look(n), Bool.pick(List<&2, HI.Row>, sheets, rs, Nil{}), at, x1, h)]) + Lay.chip(ty, (x1 - 16 - Lay.button.w(ty, Side.head(k, S.Nui.lfocus(n))) : U32), 60, Side.head(k, S.Nui.lfocus(n)), open, "vw-layers", ""), + Side.ground(ty, n, (x1 - 22 - Lay.button.w(ty, Side.head(k, S.Nui.lfocus(n))) - 28 : U32)), + Side.list.if(open, ty, k, ss, S.Nui.look(n), S.Nui.lfocus(n), Bool.pick(List<&2, HI.Row>, sheets, rs, Nil{}), at, x1, h)]) case False{}: Out.none() diff --git a/src/app/main.bend b/src/app/main.bend index 39326d2f43..d0d11a4f95 100644 --- a/src/app/main.bend +++ b/src/app/main.bend @@ -1708,7 +1708,7 @@ def Shell.base3(draft: Bool, +ww: U32, +hh: U32, +x0: U32, ts: FA.Scene3, +c: MS case False{}: FA.Scene3.render.q(True{}, ww, hh, x0, 52, (ww - x0 : U32), (hh - 52 : U32), ts, c, now, bg) -def Shell.base(+ww: U32, +hh: U32, +x0: U32, +ss: List<&2, V.Seen>, +v: V.View, +now: Nat, +flip: U32, +drag: Bool, x: V.Vx) -> Image: +def Shell.base(+ww: U32, +hh: U32, +x0: U32, +ss: List<&2, V.Seen>, +v: V.View, +now: Nat, +flip: U32, +fk: U32, +drag: Bool, x: V.Vx) -> Image: match x: case V.Vx{2, _, ts, c, _, _, _, _, _, _, bm}: R.Frame.over(ww, hh, [D.Draw.rect(D.Clip.all(), 264, 0, (x0 - 264 : U32), hh, L.Theme.ground(flip), 255)], Shell.base3(drag, ww, hh, x0, ts, c, now, V.Look.ground(flip))) @@ -1716,25 +1716,25 @@ def Shell.base(+ww: U32, +hh: U32, +x0: U32, +ss: List<&2, V.Seen>, +v: V.View, Shell.shot.base(ww, hh, x0, flip, bm) case V.Vx{+k, _, _, _, _, _, _, _, _, _, bm}: FR.Fresh.frame(ww, hh, List.reverse(&2, R.Op, D.Draw.rect(D.Clip.all(), x0, 52, (ww - x0 : U32), hh, V.Look.ground(flip), 255) - <> V.Ops.scene(ss, v, D.Clip{x0, 52, ww, hh}, now, now, True{}, k, flip)), L.Theme.ground(flip)) + <> V.Ops.scene(ss, v, D.Clip{x0, 52, ww, hh}, now, now, True{}, U32.or(k, fk), flip)), L.Theme.ground(flip)) -def Shell.vw.k(redraw: Bool, vw: V.Vw, +ww: U32, +hh: U32, +x0: U32, +now: Nat, +flip: U32) -> V.Vw: +def Shell.vw.k(redraw: Bool, vw: V.Vw, +ww: U32, +hh: U32, +x0: U32, +now: Nat, +flip: U32, +fk: U32) -> V.Vw: match redraw vw: case True{} V.Vw{+ss, +v, f, r, +d, lx, ly, _, _, _, +vx}: - V.Vw{ss, v, f, r, d, lx, ly, Shell.base(ww, hh, x0, ss, v, now, flip, d, vx), True{}, now, vx} + V.Vw{ss, v, f, r, d, lx, ly, Shell.base(ww, hh, x0, ss, v, now, flip, fk, d, vx), True{}, now, vx} case False{} _: vw # refresh the base if the scene or view changed, or fades ended since -def Shell.vw(+vw: V.Vw, +ww: U32, +hh: U32, +x0: U32, +now: Nat, +flip: U32) -> V.Vw: +def Shell.vw(+vw: V.Vw, +ww: U32, +hh: U32, +x0: U32, +now: Nat, +flip: U32, +fk: U32) -> V.Vw: match vw: case V.Vw{+ss, +v, f, r, d, lx, ly, b, +fr, +cut, vx}: - Shell.vw.k(Bool.or(Bool.or(Bool.not(fr), V.Vw.late(ss, cut, now)), FA.Scene3.fading(Vx.scene(vx), cut)), V.Vw{ss, v, f, r, d, lx, ly, b, fr, cut, vx}, ww, hh, x0, now, flip) + Shell.vw.k(Bool.or(Bool.or(Bool.not(fr), V.Vw.late(ss, cut, now)), FA.Scene3.fading(Vx.scene(vx), cut)), V.Vw{ss, v, f, r, d, lx, ly, b, fr, cut, vx}, ww, hh, x0, now, flip, fk) -def Shell.vw.if(open: Bool, +vw: V.Vw, +ww: U32, +hh: U32, +x0: U32, +ms: Nat, +flip: U32) -> V.Vw: +def Shell.vw.if(open: Bool, +vw: V.Vw, +ww: U32, +hh: U32, +x0: U32, +ms: Nat, +flip: U32, +fk: U32) -> V.Vw: match open: case True{}: - Shell.vw(vw, ww, hh, x0, ms, flip) + Shell.vw(vw, ww, hh, x0, ms, flip, fk) case False{}: vw @@ -1745,21 +1745,21 @@ def Vx.pick(x: V.Vx) -> String: # UI ops over the fading items and the picked item, over the base; tiles # the last frame drew the same way are kept (the plan of TI.Frame.par) -def Shell.image.over(+ops: List<&2, R.Op>, vw: V.Vw, +ww: U32, +hh: U32, +x0: U32, +now: Nat, +flip: U32, +hs: List<&2, N.Hit>, +pics: Map<&2, BM.Bitmap>, prev: TI.Frames, +dy: U32) -> TI.Plan: +def Shell.image.over(+ops: List<&2, R.Op>, vw: V.Vw, +ww: U32, +hh: U32, +x0: U32, +now: Nat, +flip: U32, +fk: U32, +hs: List<&2, N.Hit>, +pics: Map<&2, BM.Bitmap>, prev: TI.Frames, +dy: U32) -> TI.Plan: match vw: case V.Vw{+ss, +v, _, _, _, _, _, +b, _, +cut, +vx}: TI.Frame.plan(ww, hh, List.append(&2, R.Op, L.Theme.ops(V.Look.ui(flip), ops), List.reverse(&2, R.Op, - List.append(&2, R.Op, Bool.pick(List<&2, R.Op>, U32.is_eq(Vx.kind(vx), 2), Nil{}, V.Ops.scene(ss, v, D.Clip{x0, 52, ww, hh}, now, cut, False{}, Vx.kind(vx), flip)), + List.append(&2, R.Op, Bool.pick(List<&2, R.Op>, U32.is_eq(Vx.kind(vx), 2), Nil{}, V.Ops.scene(ss, v, D.Clip{x0, 52, ww, hh}, now, cut, False{}, U32.or(Vx.kind(vx), fk), flip)), V.Ops.picked(ss, Vx.pick(vx), v, D.Clip{x0, 52, ww, hh}, flip)))), Pics.over(hs, pics, Nil{}, ww, hh, b), prev, dy) # the frame: UI ops over the fading items over the base. dy is how far # the thread scrolled since the last frame: tiles it moved are taken from # there, not drawn again -def Shell.image(pa: Bool, +ops: List<&2, R.Op>, +vw: V.Vw, +ww: U32, +hh: U32, +x0: U32, +now: Nat, +flip: U32, +hs: List<&2, N.Hit>, +pics: Map<&2, BM.Bitmap>, prev: TI.Frames, +dy: U32) -> TI.Plan: +def Shell.image(pa: Bool, +ops: List<&2, R.Op>, +vw: V.Vw, +ww: U32, +hh: U32, +x0: U32, +now: Nat, +flip: U32, +fk: U32, +hs: List<&2, N.Hit>, +pics: Map<&2, BM.Bitmap>, prev: TI.Frames, +dy: U32) -> TI.Plan: match pa: case True{}: - Shell.image.over(ops, vw, ww, hh, x0, now, flip, hs, pics, prev, dy) + Shell.image.over(ops, vw, ww, hh, x0, now, flip, fk, hs, pics, prev, dy) case False{}: TI.Frame.plan(ww, hh, L.Theme.ops(V.Look.ui(flip), ops), Pics.over(hs, pics, Nil{}, ww, hh, Pix{L.Theme.ground(flip)}), prev, dy) @@ -1894,7 +1894,7 @@ def Lbv.paint(on: Bool, +ops: List<&2, R.Op>, +hs: List<&2, N.Hit>, +oh: List<&2 +s : N.Step = Shell.hits(n, hs, epoch) Shell.shown(r, Shell.hits.nui(s), q, i, wa, Shell.hits.anim(s, Shell.anim(n, ms)), ms) case False{}: - Shell.present(Shell.image(N.Nui.panel(n), ops, N.Nui.vw(n), N.Nui.width(n), N.Nui.height(n), N.Nui.vx0(n), ms, N.Nui.look(n), hs, Tie.pics(Aux.tie(wa)), Aux.frames(wa), N.Hits.slide(hs, oh)), + Shell.present(Shell.image(N.Nui.panel(n), ops, N.Nui.vw(n), N.Nui.width(n), N.Nui.height(n), N.Nui.vx0(n), ms, N.Nui.look(n), N.Nui.lfocus.bits(n), hs, Tie.pics(Aux.tie(wa)), Aux.frames(wa), N.Hits.slide(hs, oh)), L.Out{ops, hs}, n, w, q, i, wa, epoch, ms, t0, t2) def Shell.paint.out(o: L.Out, +oh: List<&2, N.Hit>, +n: N.Nui, w: Window, +q: Chan(ShellMsg), +i: Chan(S.Msg), +wa: Aux, +epoch: Nat, +ms: Nat, +t0: U32, +t2: U32) -> IO(Shell): @@ -1979,7 +1979,7 @@ def Shell.paint.k(+n0: N.Nui, w: Window, +q: Chan(ShellMsg), +i: Chan(S.Msg), wa match n0: case N.Nui{u, +ww, +hh, mx, my, sd, sc, ca, fo, pa, oh, _, vw0}: Shell.paint.go(oh, N.Nui{u, ww, hh, mx, my, sd, sc, ca, fo, pa, Nil{}, ms, - Shell.vw.if(N.Nui.panel(n0), vw0, ww, hh, N.Nui.vx0(n0), ms, N.Nui.look(n0))}, w, q, i, wa, epoch, ms) + Shell.vw.if(N.Nui.panel(n0), vw0, ww, hh, N.Nui.vx0(n0), ms, N.Nui.look(n0), N.Nui.lfocus.bits(n0))}, w, q, i, wa, epoch, ms) # what a frame notes before it is laid out: the hover's and the caret's # starts, the lightbox's ease, the cats' clock diff --git a/src/app/nui.bend b/src/app/nui.bend index 4e384ff1a3..27e2b85621 100644 --- a/src/app/nui.bend +++ b/src/app/nui.bend @@ -21,6 +21,7 @@ import ../core/hist.bend as HS import ../core/rview.bend as RV import ../core/emb.bend as EM import ../core/onboard.bend as OB +import ../core/layers.bend as LY # The native window's state around the shared client state (Ui): size, # pointer, scroll, focus and the composer caret. Input is pure: an event @@ -810,6 +811,18 @@ def Nui.flip.key(+n: Nui) -> String: def Nui.flip(+n: Nui) -> U32: Maybe.default(&2, U32, U32.read(U.Ui.scratch(Nui.ui(n), Nui.flip.key(n))), 0) +# the layer focus of the kind on show (core/layers.bend: the active layer +# and how the others show), kept per kind like the flips +def Nui.lfocus.key(+n: Nui) -> String: + "@vw.focus." ++ U32.show(Vw.kind(Nui.vw(n))) + +def Nui.lfocus(+n: Nui) -> LY.Focus: + LY.Lyr.read(U.Ui.scratch(Nui.ui(n), Nui.lfocus.key(n))) + +# the focus as the viewer's kind word carries it (LY.Lyr.kind.with) +def Nui.lfocus.bits(+n: Nui) -> U32: + LY.Lyr.kind.with(0, Nui.lfocus(n)) + # the viewer's light ground (its sun/moon button, "@vw.light"), and the # window's light theme (Settings, Appearance: the hub's "ui.theme") def Nui.vlight(+n: Nui) -> Bool: @@ -1327,7 +1340,7 @@ def Keys.name.k(ctrl: Bool, shift: Bool, term: Bool, +c: U32) -> String: match ctrl shift term: case True{} False{} False{}: Bool.pick(String, U32.is_eq(c, 106), "term", Bool.pick(String, U32.is_eq(c, 100), "diff", Bool.pick(String, U32.is_eq(c, 110), "new", Bool.pick(String, U32.is_eq(c, 107), "palette", - Bool.pick(String, U32.is_eq(c, 112), "files", Bool.pick(String, Bool.and(U32.is_ge(c, 49), U32.is_le(c, 57)), "jump", "")))))) + Bool.pick(String, U32.is_eq(c, 112), "files", Bool.pick(String, Bool.and(U32.is_ge(c, 49), U32.is_le(c, 57)), "jump", Bool.pick(String, U32.is_eq(c, 104), "layers", ""))))))) case True{} False{} True{}: Bool.pick(String, U32.is_eq(c, 106), "term", "") case True{} True{} _: @@ -1595,8 +1608,16 @@ def Nui.goto.k(none: Bool, +n: Nui, +pid: String, +id: String) -> Step: def Nui.goto(+n: Nui, +pid: String) -> Step: Bool.pick(Step, String.is_empty(pid), Step{n, Nil{}, False{}}, Nui.goto.k(String.is_empty(U.Side.home(Nui.ui(n), pid)), n, pid, U.Side.home(Nui.ui(n), pid))) +# the focus changed: the scene is drawn again under it +def Nui.lfocus.set(+n: Nui, +f: LY.Focus) -> Step: + Step{Nui.with_vw(Nui.with_ui(n, U.Ui.scratch_set(Nui.ui(n), Nui.lfocus.key(n), LY.Lyr.show(f))), Vw.stale(Nui.vw(n))), Nil{}, True{}} + +# the kind of the scene on show (0 a board, 1 a schematic) +def Nui.look.kind(+n: Nui) -> U32: + Vw.kind(Nui.vw(n)) + def Keys.list() -> List<&2, String>: - ["term", "diff", "new", "prev", "next", "settle", "pin", "paste", "palette", "jump", "search", "files", "project", "perf"] + ["term", "diff", "new", "prev", "next", "settle", "pin", "paste", "palette", "jump", "search", "files", "project", "perf", "layers"] def Nui.shortcut.k(i: Nat, +n: Nui, +code: U32) -> Step: match i: @@ -1626,6 +1647,10 @@ def Nui.shortcut.k(i: Nat, +n: Nui, +code: U32) -> Step: Nui.goto(n, U.Side.nth(U.Side.projects(Nui.ui(n)), U32.to_nat((code - 49 : U32)))) case 13n: Nui.act(n, "flag", "perf") + # ctrl+h: KiCad's high contrast, the other layers as they are, dimmed, + # hidden and around again (core/layers.bend's Lyr.cycle) + case 14n: + Nui.lfocus.set(n, LY.Lyr.cycle(Nui.look.kind(n), Nui.lfocus(n))) case _: Step{n, [NPaste{}], False{}} @@ -1883,10 +1908,10 @@ def Vw.press(v: V.Vw, +x: U32, +y: U32) -> V.Vw: case V.Vw{s, vi, f, r, _, _, _, b, fr, cu, V.Vx{k, p, t, c, sh, bu, _, _, pa, se, bm}}: V.Vw{s, vi, f, r, True{}, x, y, b, fr, cu, V.Vx{k, p, t, c, sh, bu, x, y, pa, se, bm}} -def Vw.release.k(click: Bool, v: V.Vw, +x: U32, +y: U32, +flip: U32) -> V.Vw: +def Vw.release.k(click: Bool, v: V.Vw, +x: U32, +y: U32, +flip: U32, +fk: U32) -> V.Vw: match click v: case True{} V.Vw{+s, +vi, f, r, _, lx, ly, b, fr, cu, V.Vx{+k, _, t, c, sh, bu, px, py, pa, se, bm}}: - V.Vw{s, vi, f, r, False{}, lx, ly, b, fr, cu, V.Vx{k, Bool.pick(String, U32.is_eq(k, 2), "", V.Pick.at(s, vi, U32.to_f32(x), U32.to_f32(y), k, flip)), t, c, sh, bu, px, py, pa, se, bm}} + V.Vw{s, vi, f, r, False{}, lx, ly, b, fr, cu, V.Vx{k, Bool.pick(String, U32.is_eq(k, 2), "", V.Pick.at(s, vi, U32.to_f32(x), U32.to_f32(y), U32.or(k, fk), flip)), t, c, sh, bu, px, py, pa, se, bm}} case False{} V.Vw{s, vi, f, r, _, lx, ly, b, fr, cu, +vx}: V.Vw{s, vi, f, r, False{}, lx, ly, b, Bool.and(fr, Bool.not(U32.is_eq(Vx.kind(vx), 2))), cu, vx} @@ -1904,15 +1929,15 @@ def Vw.dragging(v: V.Vw) -> Bool: case V.Vw{_, _, _, _, d, _, _, _, _, _, _}: d -def Vw.release(+v: V.Vw, +x: U32, +y: U32, +flip: U32) -> V.Vw: - Vw.release.k(Bool.and(Vw.dragging(v), Vw.near(Vx.press_at(Vw.x(v)), x, y)), v, x, y, flip) +def Vw.release(+v: V.Vw, +x: U32, +y: U32, +flip: U32, +fk: U32) -> V.Vw: + Vw.release.k(Bool.and(Vw.dragging(v), Vw.near(Vx.press_at(Vw.x(v)), x, y)), v, x, y, flip, fk) def Nui.view_press(+n: Nui, +x: U32, +y: U32, down: Bool) -> Step: match down: case True{}: Step{Nui.with_vw(n, Vw.press(Nui.vw(n), x, y)), Nil{}, False{}} case False{}: - Step{Nui.with_vw(n, Vw.release(Nui.vw(n), x, y, Nui.flip(n))), Nil{}, Vw.dragging(Nui.vw(n))} + Step{Nui.with_vw(n, Vw.release(Nui.vw(n), x, y, Nui.flip(n), Nui.lfocus.bits(n))), Nil{}, Vw.dragging(Nui.vw(n))} def Nui.dragging(n: Nui) -> Bool: match n: @@ -2472,6 +2497,11 @@ def Nui.sheet.open(m: Maybe<&2, Nat>, +n: Nui) -> Step: def Nui.layers.set(+n: Nui, +flip: U32) -> Step: Step{Nui.with_vw(Nui.with_ui(n, U.Ui.scratch_set(Nui.ui(n), Nui.flip.key(n), U32.show(flip))), Vw.stale(Nui.vw(n))), Nil{}, True{}} +# Reset layers: every layer as it is by default, no focus +def Nui.layers.reset(+n: Nui) -> Step: + Nui.layers.set(Nui.with_ui(n, U.Ui.scratch_set(Nui.ui(n), Nui.lfocus.key(n), "")), 0) + + # the viewer's sun/moon button: its own light or dark ground, whatever # the window's theme; the scene is drawn again on it def Nui.vlight.flip(+n: Nui) -> Step: @@ -2487,7 +2517,7 @@ def Nui.vw_act.more(i: Nat, +n: Nui, +value: String) -> Step: case 1n: Nui.layers.set(n, V.Look.toggle(Nui.flip(n), Maybe.default(&2, U32, U32.read(value), 31))) case 2n: - Nui.layers.set(n, 0) + Nui.layers.reset(n) case 3n: Nui.sheet.go(n, Maybe.default(&2, Nat, Nat.read(value), 0n)) case 4n: @@ -2498,6 +2528,12 @@ def Nui.vw_act.more(i: Nat, +n: Nui, +value: String) -> Step: Nui.vlight.flip(n) case 7n: Nui.shortcut.k(U.Keys.pos(Keys.list(), value), n, 0) + # a click on a layer's name: it stands out (LY.Lyr.pick) + case 8n: + Nui.lfocus.set(n, LY.Lyr.pick(Nui.lfocus(n), LY.Lyr.num(value, LY.Lyr.none()))) + # the list's All, Dim, Only + case 9n: + Nui.lfocus.set(n, LY.Lyr.set(Nui.look.kind(n), Nui.lfocus(n), LY.Lyr.num(value, 0))) case _: Step{n, Nil{}, False{}} @@ -2524,7 +2560,7 @@ def Nui.vw_act.k(i: Nat, +n: Nui, +value: String) -> Step: # the window's own actions (they never reach the client state) def Nui.vw_act(+n: Nui, +action: String, +value: String) -> Step: - Nui.vw_act.k(Nui.pos(["vw-kind", "vw-unpick", "vw-pal", "vw-palx", "vw-proj", "vw-projx", "vw-projo", "vw-find", "vw-layers", "vw-layer", "vw-layers-reset", "vw-sheet", "vw-open", "vw-up", "vw-light", "vw-key"], action, 0n), n, value) + Nui.vw_act.k(Nui.pos(["vw-kind", "vw-unpick", "vw-pal", "vw-palx", "vw-proj", "vw-projx", "vw-projo", "vw-find", "vw-layers", "vw-layer", "vw-layers-reset", "vw-sheet", "vw-open", "vw-up", "vw-light", "vw-key", "vw-layer-pick", "vw-layer-mode"], action, 0n), n, value) # a link in a message: only a web address is ever opened (law link_no_script) def Nui.click.url(url: Bool, +n: Nui, +action: String, +value: String) -> Step: diff --git a/src/app/viewer.bend b/src/app/viewer.bend index 81b3831bec..63aa657e38 100644 --- a/src/app/viewer.bend +++ b/src/app/viewer.bend @@ -8,6 +8,7 @@ import ../gfx/mesh.bend as MS import ../gfx/face.bend as FA import ../gfx/bitmap.bend as BM import ../core/look.bend as LO +import ../core/layers.bend as LY # The board viewer. A scene is the board's items, each with the time it # appeared, kept in paint order: by layer, then key. A new file merges into @@ -315,7 +316,7 @@ def Look.of(+kind: U32, +layer: U32) -> U32 & U32: # (a comparison's marks included: other scenes have nothing on them) def Look.count(+kind: U32) -> Nat: - LO.Look.count(U32.or(kind, LO.Look.diff())) + LO.Look.count(U32.or(LY.Lyr.kind.base(kind), LO.Look.diff())) # Layer visibility # ---------------- @@ -386,12 +387,18 @@ def Look.ground(+flip: U32) -> U32: def Look.on(+kind: U32, +layer: U32, +flip: U32) -> U32 & U32: Look.flip.k(Look.flipped(flip, layer), LO.Look.of.t(Look.light(flip), Bool.pick(U32, Look.cmp(flip), U32.or(kind, LO.Look.diff()), kind), layer)) +# as the scene draws it: Look.on, then the focus the kind word carries +# (core/layers.bend's Lyr.kind.with: the active layer stands out, the rest +# dimmed or hidden); a kind with no focus looks as Look.on says +def Look.at(+kind: U32, +layer: U32, +flip: U32) -> U32 & U32: + LY.Lyr.shade(LY.Lyr.kind.base(kind), LY.Lyr.kind.focus(kind), layer, Look.on(LY.Lyr.kind.base(kind), layer, flip)) + def Look.alpha.k(l: U32 & U32) -> U32: (_, a) = l a def Look.alpha(+kind: U32, +layer: U32, +flip: U32) -> U32: - Look.alpha.k(Look.on(kind, layer, flip)) + Look.alpha.k(Look.at(kind, layer, flip)) # whether a layer shows def Look.shows(+kind: U32, +layer: U32, +flip: U32) -> Bool: @@ -433,7 +440,7 @@ def Cull.meets(b: D.Box, +v: View, c: D.Clip) -> Bool: def Ops.pick(keep: Bool, +p: B.Prim, +layer: U32, +v: View, +c: D.Clip, +fade: U32, +kind: U32, +flip: U32, rest: List<&2, R.Op>) -> List<&2, R.Op>: match keep: case True{}: - Ops.item.k(Look.on(kind, layer, flip), p, v, c, fade, rest) + Ops.item.k(Look.at(kind, layer, flip), p, v, c, fade, rest) case False{}: rest @@ -543,13 +550,27 @@ def Ops.gate(keep: Bool, +p: B.Prim, +layer: U32, +v: View, +c: D.Clip, +now: Na # past the kind's count, hidden layers and items off the panel make none. # The cheap test (base or not) comes first, so a settled scene's overlay # costs one comparison per item. -def Ops.scene(ss: List<&2, Seen>, +v: View, +c: D.Clip, +now: Nat, +cut: Nat, +base: Bool, +kind: U32, +flip: U32) -> List<&2, R.Op>: +# (lift: the one layer drawn apart, LY.Lyr.lift: 255 none) +def Ops.scene.k(ss: List<&2, Seen>, +v: View, +c: D.Clip, +now: Nat, +cut: Nat, +base: Bool, +kind: U32, +flip: U32, +lift: U32) -> List<&2, R.Op>: match ss: case Nil{}: Nil{} case Seen{B.Item{_, _, +l, +p, _}, +b} <> t: - Ops.gate(Bool.not(Bool.xor(base, Vw.settled(b, cut))), p, l, v, c, now, b, kind, flip, - Ops.scene(t, v, c, now, cut, base, kind, flip)) + Ops.gate(Bool.and(Bool.not(Bool.xor(base, Vw.settled(b, cut))), LY.Lyr.lift.ok(lift, l)), p, l, v, c, now, b, kind, flip, + Ops.scene.k(t, v, c, now, cut, base, kind, flip, lift)) + +def Ops.scene.two(on: Bool, +ss: List<&2, Seen>, +v: View, +c: D.Clip, +now: Nat, +cut: Nat, +base: Bool, +kind: U32, +flip: U32) -> List<&2, R.Op>: + match on: + case True{}: + List.append(&2, R.Op, Ops.scene.k(ss, v, c, now, cut, base, kind, flip, LY.Lyr.lift.under(LY.Lyr.kind.focus(kind))), + Ops.scene.k(ss, v, c, now, cut, base, kind, flip, LY.Lyr.lift.over(LY.Lyr.kind.focus(kind)))) + case False{}: + Ops.scene.k(ss, v, c, now, cut, base, kind, flip, LY.Lyr.lift.none()) + +# the scene's ops in paint order; under a focus (core/layers.bend) the +# active layer after all the others, as KiCad draws it on top +def Ops.scene(+ss: List<&2, Seen>, +v: View, +c: D.Clip, +now: Nat, +cut: Nat, +base: Bool, +kind: U32, +flip: U32) -> List<&2, R.Op>: + Ops.scene.two(LY.Lyr.on(LY.Lyr.kind.focus(kind)), ss, v, c, now, cut, base, kind, flip) def Pick.circ(c: B.Pt, +r: F32) -> D.Box: match c: diff --git a/src/core/client.bend b/src/core/client.bend index 1096dd6835..87f99c9e6c 100644 --- a/src/core/client.bend +++ b/src/core/client.bend @@ -25,6 +25,7 @@ import ./apikeys.bend as AK import ./diff.bend as DF import ./fm.bend as FM import ./onboard.bend as OB +import ./layers.bend as LY # The client state, Elm-style, shared by the native window and the web # page: a host feeds server messages to Ui.recv and user actions to Ui.act, @@ -1134,7 +1135,8 @@ def Ui.actions() -> List<&2, String>: "mech", "mech-part", "mech-3d", "mech-refresh", "mech-renders", "mech-render", "view-kind", "view-sheet", "view-layer", "attach-up", "attach-end", "hist-go", "hist-prev", "hist-next", "hist-first", "hist-last", "hist-play", "hist-tick", "hist-diff", "hist-live", "hist-jump", - "hist-show", "cmp", "cmp-pick", "cmp-close", "cmp-side", "cmp-shut", "key-add", "key-del", "fm", "fm-cd", "fm-up", "fm-open", "pdf-page", "fm-page", "update-now", "work-cancel", "task-cancel"] + "hist-show", "cmp", "cmp-pick", "cmp-close", "cmp-side", "cmp-shut", "key-add", "key-del", "fm", "fm-cd", "fm-up", "fm-open", "pdf-page", "fm-page", "update-now", "work-cancel", "task-cancel", + "view-layer-pick", "view-layer-mode"] # Reading the client state # ------------------------ @@ -4194,6 +4196,33 @@ def Pdf.pages(+u: Ui) -> String: def Pdf.err(+u: Ui) -> String: Ui.info(u, "pdf.err:" ++ Ui.view_get(u, "view.path")) +# The web's layer focus (core/layers.bend): the active layer and how the +# others show, for the board and for the schematic apart ("@focus.", +# beside Solid.off's "@off."); "view-layer-pick" makes a layer the +# active one (LY.Lyr.pick), "view-layer-mode" sets how the others show +# (0 as they are, 1 dimmed, 2 hidden) +def Lfocus.kind.of(+u: Ui) -> String: + Bool.pick(String, String.is_empty(Ui.view_get(u, "view.kind")), "board", Ui.view_get(u, "view.kind")) + +def Lfocus.key(+u: Ui) -> String: + "@focus." ++ Lfocus.kind.of(u) + +def Lfocus.of(+u: Ui) -> LY.Focus: + LY.Lyr.read(Ui.scratch(u, Lfocus.key(u))) + +# the look kind of what the web shows (0 a board, 1 a schematic) +def Lfocus.kind(+u: Ui) -> U32: + Bool.pick(U32, String.eq(Lfocus.kind.of(u), "schematic"), 1, 0) + +def Lfocus.put(+u: Ui, +f: LY.Focus) -> Act: + Act{Ui.scratch_set(u, Lfocus.key(u), LY.Lyr.show(f)), Nil{}} + +def Lfocus.pick(+u: Ui, +value: String) -> Act: + Lfocus.put(u, LY.Lyr.pick(Lfocus.of(u), LY.Lyr.num(value, LY.Lyr.none()))) + +def Lfocus.mode(+u: Ui, +value: String) -> Act: + Lfocus.put(u, LY.Lyr.set(Lfocus.kind(u), Lfocus.of(u), LY.Lyr.num(value, 0))) + def Fm.act(i: Nat, +u: Ui, +value: String) -> Act: match i: case 0n: @@ -4217,6 +4246,10 @@ def Fm.act(i: Nat, +u: Ui, +value: String) -> Act: Ui.rpc(u, "work.cancel", [J.KV{"thread", J.Str{Ui.sel(u)}}, J.KV{"id", J.Str{value}}]) case 8n: Ui.rpc(u, "task.cancel", [J.KV{"child", J.Str{value}}]) + case 9n: + Lfocus.pick(u, value) + case 10n: + Lfocus.mode(u, value) case _: Act{u, Nil{}} diff --git a/src/core/layers.bend b/src/core/layers.bend new file mode 100644 index 0000000000..e17415cffa --- /dev/null +++ b/src/core/layers.bend @@ -0,0 +1,271 @@ +import Base +import ./board.bend as B +import ./schematic.bend as SC +import ./look.bend as LO + +# The layer list and the focus (pure) +# =================================== +# Every client lists a board's layers in KiCad's order (its Appearance +# panel): copper from front to back, then the pads on every copper layer, +# the vias and the holes, the technical layers in front/back pairs, the +# user layers, the edge and margin, the courtyards and the fab layers; each +# named as KiCad 7+ names it. A schematic keeps its own order. A +# comparison lists its marks after. +# +# The focus is KiCad's high contrast (ctrl+H): one active layer, and the +# others shown as they are (mode 0), dimmed (1) or not at all (2). What +# stays with the active layer: the board's outline (for bearing), and with +# a copper layer the pads, vias and holes on it; a comparison's marks. + +# board.bend's Layer.names indices in KiCad's order +def Lyr.board.order() -> List<&2, U32>: + [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] + +def Lyr.upto.go(n: Nat, +i: U32, acc: List<&2, U32>) -> List<&2, U32>: + match n: + case 0n: + List.reverse(&2, U32, acc) + case 1n+k: + Lyr.upto.go(k, (i + 1 : U32), i <> acc) + +# 0, 1, ... n-1 +def Lyr.upto(+n: Nat) -> List<&2, U32>: + Lyr.upto.go(n, 0, Nil{}) + +def Lyr.sch(+kind: U32) -> Bool: + U32.is_eq(LO.Look.base(kind), 1) + +def Lyr.own(sch: Bool) -> List<&2, U32>: + match sch: + case True{}: + Lyr.upto(24n) + case False{}: + Lyr.board.order() + +def Lyr.marks(diff: Bool) -> List<&2, U32>: + match diff: + case True{}: + [26, 27, 28] + case False{}: + Nil{} + +# the order a client lists a kind's layers in (kind 0 a board, 1 a +# schematic, with LO.Look.diff() a comparison) +def Lyr.order(+kind: U32) -> List<&2, U32>: + List.append(&2, U32, Lyr.own(Lyr.sch(kind)), Lyr.marks(LO.Look.is_diff(kind))) + +# KiCad's names where they differ from the file's +def Lyr.board.label(+i: Nat, +raw: String) -> String: + match i: + case 1n: + "B.Courtyard" + case 3n: + "B.Silkscreen" + case 13n: + "Pads" + case 16n: + "F.Silkscreen" + case 17n: + "F.Courtyard" + case 19n: + "User.Drawings" + case 20n: + "User.Comments" + case 23n: + "Vias" + case 24n: + "Holes" + case _: + raw + +def Lyr.label.k(sch: Bool, +i: U32, +raw: String) -> String: + match sch: + case True{}: + raw + case False{}: + Lyr.board.label(U32.to_nat(i), raw) + +def Lyr.raw(+sch: Bool) -> List<&2, String>: + Bool.pick(List<&2, String>, sch, SC.Sch.layer_names(), B.Layer.names()) + +# a layer's name in the list +def Lyr.name(+kind: U32, +i: U32) -> String: + Lyr.label.k(Lyr.sch(kind), i, Maybe.default(&2, String, List.get(&2, String, LO.Look.names(kind, Lyr.raw(Lyr.sch(kind))), U32.to_nat(i)), "")) + +# The focus +# --------- + +type Focus is Data: + Focus{active: U32, mode: U32} + +# no active layer +def Lyr.none() -> U32: + 255 + +def Lyr.plain() -> Focus: + Focus{Lyr.none(), 0} + +def Lyr.active(f: Focus) -> U32: + match f: + case Focus{a, _}: + a + +def Lyr.mode(f: Focus) -> U32: + match f: + case Focus{_, m}: + m + +# whether the focus changes anything +def Lyr.on(+f: Focus) -> Bool: + Bool.and(U32.is_gt(Lyr.mode(f), 0), Bool.not(U32.is_eq(Lyr.active(f), Lyr.none()))) + +# kept as text in each client's state: "|" +def Lyr.show(+f: Focus) -> String: + U32.show(Lyr.active(f)) ++ "|" ++ U32.show(Lyr.mode(f)) + +def Lyr.num(+s: String, +d: U32) -> U32: + Maybe.default(&2, U32, U32.read(s), d) + +def Lyr.read.k(ps: List<&2, String>) -> Focus: + match ps: + case +a <> +m <> _: + Focus{Lyr.num(a, Lyr.none()), U32.min(Lyr.num(m, 0), 2)} + case _: + Lyr.plain() + +def Lyr.read(+s: String) -> Focus: + Lyr.read.k(String.split(s, '|')) + +# the copper layers (pads, vias and holes stay with them) +def Lyr.copper(+kind: U32, +l: U32) -> Bool: + Bool.and(Bool.not(Lyr.sch(kind)), Bool.and(U32.is_ge(l, 5), U32.is_le(l, 12))) + +def Lyr.bit(+l: U32) -> U32: + U32.shln(1, U32.to_nat(U32.min(l, 31))) + +# the layers that stay as they are beside the active one, as bits +def Lyr.kept(+kind: U32, +f: Focus) -> U32: + U32.or(U32.or(Lyr.bit(Lyr.active(f)), + Bool.pick(U32, Lyr.copper(kind, Lyr.active(f)), U32.or(U32.or(Lyr.bit(13), Lyr.bit(23)), Lyr.bit(24)), 0)), + U32.or(Bool.pick(U32, Lyr.sch(kind), 0, Lyr.bit(22)), U32.or(U32.or(Lyr.bit(26), Lyr.bit(27)), Lyr.bit(28)))) + +def Lyr.has(+bits: U32, +l: U32) -> Bool: + Bool.not(U32.is_eq(U32.and(bits, Lyr.bit(l)), 0)) + +# every layer a board, a schematic or a comparison has (bits 0-28) +def Lyr.all() -> U32: + 536870911 + +def Lyr.shade.k(on: Bool, active: Bool, kept: Bool, +mode: U32, look: U32 & U32) -> U32 & U32: + match on active kept: + case False{} _ _: + look + case True{} True{} _: + (c, +a) = look + (c, Bool.pick(U32, U32.is_eq(a, 0), 200, a)) + case True{} False{} True{}: + look + case True{} False{} False{}: + (c, +a) = look + (c, Bool.pick(U32, U32.is_eq(mode, 1), (a / 4 : U32), 0)) + +# a layer's colour and opacity under the focus: the active layer always +# shows, those kept with it as they are, the rest dimmed to a quarter or +# hidden +def Lyr.shade(+kind: U32, +f: Focus, +l: U32, look: U32 & U32) -> U32 & U32: + Lyr.shade.k(Lyr.on(f), U32.is_eq(l, Lyr.active(f)), Lyr.has(Lyr.kept(kind, f), l), Lyr.mode(f), look) + +type Masks is Data: + Masks{off: U32, dim: U32} + +# for the clients that draw from bits (the web, the phones): the layers +# not drawn (the person's own, those the focus hides, never the active +# one) and those drawn dimmed +def Lyr.masks.k(on: Bool, +kind: U32, +off: U32, +f: Focus) -> Masks: + match on: + case False{}: + Masks{off, 0} + case True{}: + +rest = {U32.and(Lyr.all(), U32.not(Lyr.kept(kind, f))) : U32} + +act = {U32.not(Lyr.bit(Lyr.active(f))) : U32} + Bool.pick(Masks, U32.is_eq(Lyr.mode(f), 1), + Masks{U32.and(off, act), U32.and(rest, U32.not(off))}, + Masks{U32.and(U32.or(off, rest), act), 0}) + +def Lyr.masks(+kind: U32, +off: U32, +f: Focus) -> Masks: + Lyr.masks.k(Lyr.on(f), kind, off, f) + +def Masks.off(m: Masks) -> U32: + match m: + case Masks{o, _}: + o + +def Masks.dim(m: Masks) -> U32: + match m: + case Masks{_, d}: + d + +# The focus changes +# ----------------- + +# the layer a focus starts from when none is active: front copper on a +# board, the first layer of a schematic +def Lyr.first(+kind: U32) -> U32: + Bool.pick(U32, Lyr.sch(kind), 0, 12) + +def Lyr.or.first(+kind: U32, +a: U32) -> U32: + Bool.pick(U32, U32.is_eq(a, Lyr.none()), Lyr.first(kind), a) + +# a click on a layer's name: it becomes the active one and the others dim +# (or stay hidden, in that mode); a click on the active one again puts +# every layer back as it was +def Lyr.pick.k(again: Bool, +f: Focus, +l: U32) -> Focus: + match again: + case True{}: + Focus{l, 0} + case False{}: + Focus{l, Bool.pick(U32, U32.is_eq(Lyr.mode(f), 0), 1, Lyr.mode(f))} + +def Lyr.pick(+f: Focus, +l: U32) -> Focus: + Lyr.pick.k(Bool.and(Lyr.on(f), U32.is_eq(Lyr.active(f), l)), f, l) + +# the list's mode switch (All, Dim, Only) +def Lyr.set(+kind: U32, +f: Focus, +mode: U32) -> Focus: + Focus{Lyr.or.first(kind, Lyr.active(f)), U32.min(mode, 2)} + +# ctrl+H, as in KiCad: as they are, dimmed, hidden, and around again +def Lyr.cycle(+kind: U32, +f: Focus) -> Focus: + Lyr.set(kind, f, Bool.pick(U32, U32.is_eq(Lyr.mode(f), 2), 0, (Lyr.mode(f) + 1 : U32))) + +# The window draws from a kind word: its kind and comparison flag in the +# low byte, the focus above (active in bits 8-15, mode in 16-17) +def Lyr.kind.with(+kind: U32, +f: Focus) -> U32: + U32.or(U32.and(kind, 255), U32.or(U32.shln(U32.and(Lyr.active(f), 255), 8n), U32.shln(U32.and(Lyr.mode(f), 3), 16n))) + +def Lyr.kind.base(+kind: U32) -> U32: + U32.and(kind, 255) + +def Lyr.kind.focus(+kind: U32) -> Focus: + Focus{U32.and(U32.shrn(kind, 8n), 255), U32.and(U32.shrn(kind, 16n), 3)} + +# The active layer drawn on top +# ------------------------------ +# A layer drawn apart from the rest of the scene: every layer but it +# first (under), then it alone (over); none draws all in one pass. As a +# number: 255 none, below that every layer but it, 256 and up it alone. + +def Lyr.lift.none() -> U32: + 255 + +def Lyr.lift.under(+f: Focus) -> U32: + U32.min(Lyr.active(f), 254) + +def Lyr.lift.over(+f: Focus) -> U32: + (U32.min(Lyr.active(f), 254) + 256 : U32) + +def Lyr.lift.ok(+lift: U32, +l: U32) -> Bool: + Bool.pick(Bool, U32.is_eq(lift, 255), True{}, Bool.pick(Bool, U32.is_lt(lift, 255), Bool.not(U32.is_eq(l, lift)), U32.is_eq(l, (lift - 256 : U32)))) + +# the layer the web and phones draw last (255 none) +def Lyr.top(+f: Focus) -> U32: + Bool.pick(U32, Lyr.on(f), Lyr.active(f), Lyr.none()) diff --git a/src/mobile/hubs.bend b/src/mobile/hubs.bend index 501a332977..e455f54836 100644 --- a/src/mobile/hubs.bend +++ b/src/mobile/hubs.bend @@ -270,11 +270,15 @@ def One.act.k(i: Nat, +u: U.Ui, +action: String, +value: String) -> U.Act: VW.View.fm.act(u, action, value) case 20n: VW.View.fm.act(u, action, value) + case 21n: + VW.View.layer.pick(u, value) + case 22n: + VW.View.layer.mode(u, value) case _: One.act.rest(Bool.or(String.starts_with(action, "hist-"), String.starts_with(action, "cmp")), u, action, value) def One.act(+u: U.Ui, +action: String, +value: String) -> U.Act: - One.act.k(C.Codec.pos(["view", "view-mention", "view-pick", "view-unpick", "picker-open", "picker-type", "picker-mkdir", "find-open", "find-q", "find-close", "form-close", "view-sheet", "view-layer", "view-parts", "mech-3d", "mech-renders", "fm", "fm-cd", "fm-up", "fm-open", "fm-page"], action), u, action, value) + One.act.k(C.Codec.pos(["view", "view-mention", "view-pick", "view-unpick", "picker-open", "picker-type", "picker-mkdir", "find-open", "find-q", "find-close", "form-close", "view-sheet", "view-layer", "view-parts", "mech-3d", "mech-renders", "fm", "fm-cd", "fm-up", "fm-open", "fm-page", "view-layer-pick", "view-layer-mode"], action), u, action, value) # what a step does to one hub's client type Op is Data: diff --git a/src/mobile/view.bend b/src/mobile/view.bend index a0b3e33ad3..551944e1c9 100644 --- a/src/mobile/view.bend +++ b/src/mobile/view.bend @@ -11,6 +11,7 @@ import ../core/mech.bend as MC import ../core/hist.bend as HS import ../core/fm.bend as FM import ../core/images.bend as IM +import ../core/layers.bend as LY # The phones' board viewer, as the Bend client decides it. The viewer # shows the selected thread's project board, schematic or 3D model @@ -330,29 +331,64 @@ def View.off(+u: U.Ui) -> U32: def View.layer(+u: U.Ui, +v: String) -> U.Act: U.Act{Pick.clear(U.Ui.scratch_set(u, View.off.key(u), U32.show(V.Look.toggle(View.off(u), Maybe.default(&2, U32, U32.read(v), 31))))), Nil{}} +# The focus (core/layers.bend): the active layer and how the others show +# ("@view.focus."); "view-layer-pick" makes a layer stand out (again, +# all back), "view-layer-mode" sets All, Dim or Only + +def View.focus.key(+u: U.Ui) -> String: + "@view.focus." ++ View.kind(u) + +def View.focus(+u: U.Ui) -> LY.Focus: + LY.Lyr.read(U.Ui.scratch(u, View.focus.key(u))) + +# the look kind (0 a board, 1 a schematic) +def View.lkind(+u: U.Ui) -> U32: + Bool.pick(U32, String.eq(View.kind(u), "schematic"), 1, 0) + +def View.focus.put(+u: U.Ui, +f: LY.Focus) -> U.Act: + U.Act{Pick.clear(U.Ui.scratch_set(u, View.focus.key(u), LY.Lyr.show(f))), Nil{}} + +def View.layer.pick(+u: U.Ui, +v: String) -> U.Act: + View.focus.put(u, LY.Lyr.pick(View.focus(u), LY.Lyr.num(v, LY.Lyr.none()))) + +def View.layer.mode(+u: U.Ui, +v: String) -> U.Act: + View.focus.put(u, LY.Lyr.set(View.lkind(u), View.focus(u), LY.Lyr.num(v, 0))) + +# what the phone draws from: the layers not drawn and those dimmed +def View.masks(+u: U.Ui) -> LY.Masks: + LY.Lyr.masks(View.lkind(u), View.off(u), View.focus(u)) + +# the list's mode switch: as it is (0), dimmed (1), hidden (2) +def View.lmode(+u: U.Ui) -> U32: + Bool.pick(U32, LY.Lyr.on(View.focus(u)), LY.Lyr.mode(View.focus(u)), 0) + +# the Layers button names the active layer while one stands out +def View.lhead(+u: U.Ui) -> String: + Bool.pick(String, LY.Lyr.on(View.focus(u)), "Layers · " ++ LY.Lyr.name(View.lkind(u), LY.Lyr.active(View.focus(u))), "Layers") + def View.names(+kind: String) -> List<&2, String>: Bool.pick(List<&2, String>, String.eq(kind, "schematic"), SC.Sch.layer_names(), B.Layer.names()) -def View.layer.row(shown: Bool, +i: U32, +name: String, +off: U32, rest: List<&2, J.Json>) -> List<&2, J.Json>: +def View.layer.row(shown: Bool, +i: U32, +name: String, +off: U32, +act: Bool, rest: List<&2, J.Json>) -> List<&2, J.Json>: match shown: case True{}: - J.Json.obj([J.KV{"layer", J.Num{U32.show(i)}}, J.KV{"name", J.Str{name}}, J.KV{"on", J.Flag{Bool.not(V.Look.flipped(off, i))}}]) <> rest + J.Json.obj([J.KV{"layer", J.Num{U32.show(i)}}, J.KV{"name", J.Str{name}}, J.KV{"on", J.Flag{Bool.not(V.Look.flipped(off, i))}}, J.KV{"active", J.Flag{act}}]) <> rest case False{}: rest -def View.layer.rows(ns: List<&2, String>, +k: U32, +i: U32, +off: U32) -> List<&2, J.Json>: - match ns: +def View.layer.rows(ls: List<&2, U32>, +k: U32, +off: U32, +f: LY.Focus) -> List<&2, J.Json>: + match ls: case Nil{}: Nil{} - case +n <> t: - View.layer.row(U32.is_gt(V.Look.alpha(k, i, 0), 0), i, n, off, View.layer.rows(t, k, (i + 1 : U32), off)) + case +i <> t: + View.layer.row(U32.is_gt(V.Look.alpha(k, i, 0), 0), i, LY.Lyr.name(k, i), off, Bool.and(LY.Lyr.on(f), U32.is_eq(i, LY.Lyr.active(f))), View.layer.rows(t, k, off, f)) -# the list the phone's Layers menu shows (none for 3D) -# (a comparison's marks too: removed, changed, added) +# the list the phone's Layers menu shows (none for 3D), in KiCad's order +# (core/layers.bend; a comparison's marks too: removed, changed, added) def View.layer.list(+u: U.Ui) -> List<&2, J.Json>: +k = {U32.or(Bool.pick(U32, String.eq(View.kind(u), "schematic"), 1, 0), Bool.pick(U32, Bool.and(View.hist(u), Nat.is_eq(HS.Hist.kind(U.Ui.view_get(u, "view.path")), 2n)), LO.Look.diff(), 0)) : U32} Bool.pick(List<&2, J.Json>, Bool.or(String.eq(View.kind(u), "board"), String.eq(View.kind(u), "schematic")), - View.layer.rows(LO.Look.names(k, View.names(View.kind(u))), k, 0, View.off(u)), Nil{}) + View.layer.rows(LY.Lyr.order(k), k, View.off(u), View.focus(u)), Nil{}) # Sheets # ------ @@ -514,7 +550,7 @@ def Pick.ask(m: Maybe<&2, Cand>, +u: U.Ui) -> U.Act: # picks is highlighted and its info asked of the hub; a tap on nothing # clears the card (law pick_nothing_there) def View.pick(+u: U.Ui, +text: String) -> U.Act: - Pick.ask(Pick.of(text, View.off(u)), u) + Pick.ask(Pick.of(text, LY.Masks.off(View.masks(u))), u) def View.picked.k(mine: Bool, +u: U.Ui, +o: J.Json) -> U.Ui: match mine: @@ -670,7 +706,9 @@ def View.json(+u: U.Ui) -> J.Json: J.KV{"layers", J.Str{Bool.pick(String, View.mech(u), "", View.layers.at(View.kind(u), U.Ui.root(u), View.src(u)))}}, J.KV{"hist", Hist.json(U.Hs.bar.for(View.as_client(u), Nat.is_lt(View.hist.num(View.kind(u)), 2n)), View.as_client(u))}, J.KV{"mech", Mech.json(View.mech(u), u)}, - J.KV{"sheets", J.Json.arr(View.sheet.list(u))}, J.KV{"layerList", J.Json.arr(View.layer.list(u))}, J.KV{"off", J.Num{U32.show(View.off(u))}}, + J.KV{"sheets", J.Json.arr(View.sheet.list(u))}, J.KV{"layerList", J.Json.arr(View.layer.list(u))}, + J.KV{"off", J.Num{U32.show(LY.Masks.off(View.masks(u)))}}, J.KV{"dim", J.Num{U32.show(LY.Masks.dim(View.masks(u)))}}, J.KV{"lift", J.Num{U32.show(LY.Lyr.top(View.focus(u)))}}, + J.KV{"layerMode", J.Num{U32.show(View.lmode(u))}}, J.KV{"layerModes", J.Flag{String.eq(View.kind(u), "board")}}, J.KV{"layerHead", J.Str{View.lhead(u)}}, J.KV{"parts", J.Flag{Bool.not(View.bare(u))}}, J.KV{"note", J.Str{View.note(u)}}, J.KV{"choices", J.Json.arr(Bool.pick(List<&2, J.Json>, String.is_empty(U.Ui.root(u)), Nil{}, [View.choice("Board", "board", View.kind(u)), View.choice("Schematic", "schematic", View.kind(u)), View.choice("3D", "3d", View.kind(u)), View.choice("Mech", "mech", Bool.pick(String, View.mech(u), "mech", "")), diff --git a/src/web/plot2d.js b/src/web/plot2d.js index 81fa8751f5..a69329670d 100644 --- a/src/web/plot2d.js +++ b/src/web/plot2d.js @@ -103,6 +103,8 @@ class View { this.key = ""; this.plot = null; this.off = 0; + this.dim = 0; + this.lift = 255; this.scale = 1; this.ox = 0; this.oy = 0; this.fitted = false; this.fade = 220; @@ -220,11 +222,15 @@ class View { g.lineJoin = "round"; const now = performance.now(); let fading = false; - for (const k of p.chunks) { + // the active layer of a focus (data-lift) after the rest, on top + const lift = this.lift; + const order = lift >= 0 && lift < 32 ? [...p.chunks.filter((k) => k.layer !== lift), ...p.chunks.filter((k) => k.layer === lift)] : p.chunks; + for (const k of order) { if (Math.floor(this.off / 2 ** k.layer) % 2) continue; // a layer turned off const a = this.fade > 0 ? Math.min(1, (now - (k.born ?? 0)) / this.fade) : 1; if (a < 1) fading = true; - g.globalAlpha = a; + // a layer the focus dims (core/layers.bend's Lyr.masks) at a quarter + g.globalAlpha = Math.floor(this.dim / 2 ** k.layer) % 2 ? a / 4 : a; g.fillStyle = k.color; g.strokeStyle = k.color; g.fill(k.fill); @@ -261,8 +267,8 @@ function present(p) { export function mount(canvas) { if (!canvas) { view = null; return; } if (!view || view.canvas !== canvas) view = new View(canvas, canvas.dataset.bg || "#0d0f12"); - const off = Number(canvas.dataset.off || 0); - if (off !== view.off) { view.off = off; view.later(); } + const off = Number(canvas.dataset.off || 0), dim = Number(canvas.dataset.dim || 0), lift = Number(canvas.dataset.lift ?? 255); + if (off !== view.off || dim !== view.dim || lift !== view.lift) { view.off = off; view.dim = dim; view.lift = lift; view.later(); } present(view.plot); view.fade = Number(canvas.dataset.fade || 0); const key = canvas.dataset.key || ""; diff --git a/src/web/style.css b/src/web/style.css index 811fe7fbbe..20514300e9 100644 --- a/src/web/style.css +++ b/src/web/style.css @@ -267,8 +267,19 @@ button.fold span { overflow: hidden; text-overflow: ellipsis; white-space: nowra .layers[open] summary { border-color: var(--accent); } .layer-list { position: absolute; top: 100%; left: 0; z-index: 4; margin-top: 4px; display: grid; gap: 2px; padding: 6px; min-width: 150px; max-height: 60vh; overflow: auto; background: var(--panel); border: 1px solid var(--line); box-shadow: 0 6px 18px rgba(0,0,0,0.35); } -.layer-list .layer { text-align: left; color: var(--faint); text-decoration: line-through; } -.layer-list .layer.on { color: var(--text); text-decoration: none; } +.layer-list { gap: 0; min-width: 190px; } +.layer-row { display: flex; align-items: center; gap: 8px; } +.layer-row[hidden] { display: none; } +.layer-row .swatch { width: 14px; height: 14px; padding: 0; border: 1px solid var(--line); flex: none; cursor: pointer; } +.layer-row.off .swatch { opacity: 0.2; } +.layer-row .lname { flex: 1; text-align: left; padding: 3px 6px; border: 0; background: none; color: var(--text); cursor: pointer; } +.layer-row.off .lname { color: var(--faint); text-decoration: line-through; } +.layer-row.active .lname { background: var(--sel, var(--raise)); color: var(--accent); } +.layer-row .lname:hover { background: var(--raise); } +.layer-modes { display: flex; gap: 0; margin-bottom: 6px; } +.layer-modes .lmode { flex: 1; padding: 3px 6px; border: 1px solid var(--line); background: var(--raise); color: var(--faint); cursor: pointer; } +.layer-modes .lmode + .lmode { border-left: 0; } +.layer-modes .lmode.on { color: var(--text); border-color: var(--accent); } .viewer-say { display: block; padding: 6px 10px; color: var(--faint); font-size: 12px; border-bottom: 1px solid var(--line); } .viewer-stage canvas { position: absolute; inset: 0; touch-action: none; cursor: grab; } .viewer-stage canvas.dragging { cursor: grabbing; } diff --git a/src/web/view.bend b/src/web/view.bend index b0665d242d..4d8f5196aa 100644 --- a/src/web/view.bend +++ b/src/web/view.bend @@ -10,6 +10,7 @@ import ../core/diff.bend as DF import ../core/mech.bend as MC import ../core/look.bend as LO import ../core/board.bend as B +import ../core/layers.bend as LY import ../core/schematic.bend as SC import ../core/hier.bend as HI import ../core/images.bend as IM @@ -1428,33 +1429,65 @@ def View.sheets(+u: U.Ui) -> List<&2, D.Node>: Bool.pick(List<&2, D.Node>, Nat.is_gt(List.length(&2, HI.Row, HI.Hier.read(U.Ui.info(u, "sheets:" ++ U.Ui.root(u)))), 1n), View.sheet.chips(HI.Hier.read(U.Ui.info(u, "sheets:" ++ U.Ui.root(u))), 0n, U.Ui.view_get(u, "view.path")), [D.Dom.span("what", "The project's schematic")]) -# the Layers menu: a button for each layer shown by default (as the -# phones list them; plot2d.js hides those the file has nothing on), lit -# while on -def View.layer.btn(shown: Bool, +i: U32, +name: String, +off: U32, rest: List<&2, D.Node>) -> List<&2, D.Node>: +# the Layers menu (core/layers.bend): each layer shown by default, in +# KiCad's order (plot2d.js hides those the file has nothing on), a row of +# its colour (a click shows or hides it) and its name (a click makes it the +# active layer and dims the rest; a click on it again puts all back). Over +# a board's rows, how the layers other than the active one show: All, Dim +# or Only. +def View.layer.alpha(l: U32 & U32) -> U32: + (_, a) = l + a + +def View.layer.color(l: U32 & U32) -> U32: + (c, _) = l + c + +def View.layer.row(shown: Bool, +i: U32, +name: String, +color: U32, +off: U32, +act: Bool, rest: List<&2, D.Node>) -> List<&2, D.Node>: match shown: case True{}: - D.El{"button", "", [D.At{"class", Bool.pick(String, U32.is_eq(U32.and(U32.shrn(off, U32.to_nat(i)), 1), 1), "layer", "layer on")}, - D.At{"title", "Show or hide " ++ name}, D.At{"data-value", U32.show(i)}, D.At{"data-layer", U32.show(i)}, D.On{"click", "view-layer"}], [D.Txt{name}]} <> rest + D.El{"div", "", [D.At{"class", "layer-row" ++ Bool.pick(String, act, " active", "") ++ Bool.pick(String, LY.Lyr.has(off, i), " off", "")}, D.At{"data-layer", U32.show(i)}], + [D.El{"button", "", [D.At{"class", "swatch"}, D.At{"title", "Show or hide " ++ name}, D.At{"data-value", U32.show(i)}, + D.At{"style", "background:" ++ View.hex(color)}, D.On{"click", "view-layer"}], Nil{}}, + D.El{"button", "", [D.At{"class", "lname"}, D.At{"title", Bool.pick(String, act, "Show every layer again", name ++ " stands out")}, D.At{"data-value", U32.show(i)}, + D.On{"click", "view-layer-pick"}], [D.Txt{name}]}]} <> rest case False{}: rest -def View.layer.alpha(l: U32 & U32) -> U32: - (_, a) = l - a - -def View.layer.btns(ns: List<&2, String>, +k: U32, +i: U32, +off: U32) -> List<&2, D.Node>: - match ns: +def View.layer.rows(ls: List<&2, U32>, +k: U32, +off: U32, +f: LY.Focus) -> List<&2, D.Node>: + match ls: case Nil{}: Nil{} - case +n <> t: - View.layer.btn(U32.is_gt(View.layer.alpha(LO.Look.of.t(False{}, k, i)), 0), i, n, off, View.layer.btns(t, k, (i + 1 : U32), off)) + case +i <> t: + View.layer.row(U32.is_gt(View.layer.alpha(LO.Look.of.t(False{}, k, i)), 0), i, LY.Lyr.name(k, i), View.layer.color(LO.Look.of.t(False{}, k, i)), off, + Bool.and(LY.Lyr.on(f), U32.is_eq(i, LY.Lyr.active(f))), View.layer.rows(t, k, off, f)) + +def View.layer.mode(+m: U32, +cur: U32, +label: String, +tip: String) -> D.Node: + D.El{"button", "", [D.At{"class", Bool.pick(String, U32.is_eq(m, cur), "lmode on", "lmode")}, D.At{"title", tip}, D.At{"data-value", U32.show(m)}, D.On{"click", "view-layer-mode"}], [D.Txt{label}]} + +# KiCad's high contrast: the other layers as they are, dimmed, or hidden +def View.layer.modes(sch: Bool, +f: LY.Focus) -> List<&2, D.Node>: + match sch: + case True{}: + Nil{} + case False{}: + +cur = {Bool.pick(U32, LY.Lyr.on(f), LY.Lyr.mode(f), 0) : U32} + [D.El{"div", "", [D.At{"class", "layer-modes"}], [View.layer.mode(0, cur, "All", "Every layer as it is"), + View.layer.mode(1, cur, "Dim", "The active layer stands out, the rest dimmed"), + View.layer.mode(2, cur, "Only", "Only the active layer, with the outline (and a copper layer's pads and vias)")]}] + +# the menu's button names the active layer while one stands out +def View.layer.head(+k: U32, +f: LY.Focus) -> String: + Bool.pick(String, LY.Lyr.on(f), "Layers · " ++ LY.Lyr.name(k, LY.Lyr.active(f)), "Layers") + +# the look kind of a plot: a board or schematic, a comparison's marks too +def View.layer.kind(sch: Bool, cmp: Bool) -> U32: + U32.or(Bool.pick(U32, sch, 1, 0), Bool.pick(U32, cmp, LO.Look.diff(), 0)) -# (a comparison lists its marks too: removed, changed, added) -def View.layers(+sch: Bool, +cmp: Bool, +off: U32) -> D.Node: - +k = {U32.or(Bool.pick(U32, sch, 1, 0), Bool.pick(U32, cmp, LO.Look.diff(), 0)) : U32} - D.El{"details", "", [D.At{"class", "layers"}], [D.El{"summary", "", [D.At{"title", "Turn layers on and off"}], [D.Txt{"Layers"}]}, - D.El{"div", "", [D.At{"class", "layer-list"}], View.layer.btns(LO.Look.names(k, Bool.pick(List<&2, String>, sch, SC.Sch.layer_names(), B.Layer.names())), k, 0, off)}]} +def View.layers(+sch: Bool, +cmp: Bool, +off: U32, +f: LY.Focus) -> D.Node: + +k = View.layer.kind(sch, cmp) + D.El{"details", "", [D.At{"class", "layers"}], [D.El{"summary", "", [D.At{"title", "Layers: show, hide or single one out"}], [D.Txt{View.layer.head(k, f)}]}, + D.El{"div", "", [D.At{"class", "layer-list"}], List.append(&2, D.Node, View.layer.modes(sch, f), View.layer.rows(LY.Lyr.order(k), k, off, f))}]} # The design history bar (client.bend's Hs.bar): the arrows, play and # the position, the changes, compare and live buttons; a scrubber with a @@ -1554,8 +1587,10 @@ def View.hist.menu(+u: U.Ui) -> D.Node: # a board or schematic: host.js (plot2d.js) draws the hub's plot of it on # the canvas whose data-key it carries, less the layers data-off names def View.plot(+u: U.Ui, +what: List<&2, D.Node>) -> List<&2, D.Node>: - View.bar(List.append(&2, D.Node, what, [View.layers(String.eq(U.Solid.kind(u), "schematic"), Nat.is_eq(HS.Hist.kind(U.Ui.view_get(u, "view.path")), 2n), U.Solid.off(u))]), "drag or two fingers to pan · wheel or pinch to zoom · double-click to fit") - <> D.El{"div", "", [D.At{"class", "viewer-stage"}], [D.El{"canvas", "plot", [D.At{"id", "plot"}, D.At{"data-key", U.Solid.want(u)}, D.At{"data-off", U32.show(U.Solid.off(u))}, + View.bar(List.append(&2, D.Node, what, [View.layers(String.eq(U.Solid.kind(u), "schematic"), Nat.is_eq(HS.Hist.kind(U.Ui.view_get(u, "view.path")), 2n), U.Solid.off(u), U.Lfocus.of(u))]), "drag or two fingers to pan · wheel or pinch to zoom · double-click to fit") + <> D.El{"div", "", [D.At{"class", "viewer-stage"}], [D.El{"canvas", "plot", [D.At{"id", "plot"}, D.At{"data-key", U.Solid.want(u)}, + D.At{"data-off", U32.show(LY.Masks.off(LY.Lyr.masks(U.Lfocus.kind(u), U.Solid.off(u), U.Lfocus.of(u))))}, + D.At{"data-dim", U32.show(LY.Masks.dim(LY.Lyr.masks(U.Lfocus.kind(u), U.Solid.off(u), U.Lfocus.of(u))))}, D.At{"data-lift", U32.show(LY.Lyr.top(U.Lfocus.of(u)))}, D.At{"data-fade", Nat.show(U.Hs.fade())}, D.At{"data-fam", HS.Hist.key.family(U.Solid.want(u))}], Nil{}}, View.hist.menu(u)]} <> View.hist(U.Hs.bar(u), u) diff --git a/test/layers_test.bend b/test/layers_test.bend new file mode 100644 index 0000000000..e447d2cd33 --- /dev/null +++ b/test/layers_test.bend @@ -0,0 +1,62 @@ +import Base +import ./check.bend as C +import ../src/core/layers.bend as L + +# The layer list and the focus (core/layers.bend): KiCad's order and +# names, the active layer with the rest dimmed or hidden, the masks the +# web and phones draw from, and the window's kind word. + +def ns(ls: List<&2, U32>) -> String: + match ls: + case Nil{}: + "" + case +l <> +t: + L.Lyr.name(0, l) ++ Bool.pick(String, List.is_empty(&2, U32, t), "", " ") ++ ns(t) + +def lk(l: U32 & U32) -> String: + (c, a) = l + U32.show(c) ++ "/" ++ U32.show(a) + +def fs(+f: L.Focus) -> String: + L.Lyr.show(f) + +def ms(+m: L.Masks) -> String: + U32.show(L.Masks.off(m)) ++ "/" ++ U32.show(L.Masks.dim(m)) + +def bool(b: Bool) -> String: + Bool.pick(String, b, "true", "false") + +def main() -> IO(Unit): + do IO: + C.Check.str("a board's layers in KiCad's order", ns(L.Lyr.order(0)), + "F.Cu In1.Cu In2.Cu In3.Cu In4.Cu In5.Cu In6.Cu B.Cu Pads Vias Holes F.Paste B.Paste F.Silkscreen B.Silkscreen F.Mask B.Mask User.Drawings User.Comments Edge.Cuts Margin F.Courtyard B.Courtyard F.Fab B.Fab") + C.Check.str("a schematic keeps its order", U32.show(Maybe.default(&2, U32, List.head(&2, U32, L.Lyr.order(1)), 99)), "0") + C.Check.str("a comparison lists its marks last", L.Lyr.name(8, 28), "Added") + C.Check.str("no focus: as it is", lk(L.Lyr.shade(0, L.Lyr.plain(), 16, (7, 200))), "7/200") + C.Check.str("dimmed: another layer at a quarter", lk(L.Lyr.shade(0, L.Focus{12, 1}, 16, (7, 200))), "7/50") + C.Check.str("only: another layer hidden", lk(L.Lyr.shade(0, L.Focus{12, 2}, 16, (7, 200))), "7/0") + C.Check.str("only: the active layer shows", lk(L.Lyr.shade(0, L.Focus{12, 2}, 12, (7, 210))), "7/210") + C.Check.str("a layer hidden by default shows when active", lk(L.Lyr.shade(0, L.Focus{15, 2}, 15, (7, 0))), "7/200") + C.Check.str("copper keeps its vias", lk(L.Lyr.shade(0, L.Focus{5, 2}, 23, (7, 255))), "7/255") + C.Check.str("and its pads", lk(L.Lyr.shade(0, L.Focus{5, 2}, 13, (7, 220))), "7/220") + C.Check.str("silkscreen does not", lk(L.Lyr.shade(0, L.Focus{16, 2}, 23, (7, 255))), "7/0") + C.Check.str("the outline stays", lk(L.Lyr.shade(0, L.Focus{16, 2}, 22, (7, 255))), "7/255") + C.Check.str("masks: none", ms(L.Lyr.masks(0, 5, L.Lyr.plain())), "5/0") + C.Check.str("masks: only F.Cu hides the rest but its own", ms(L.Lyr.masks(0, 0, L.Focus{12, 2})), "37736447/0") + C.Check.str("masks: dim F.Cu dims the rest", ms(L.Lyr.masks(0, 0, L.Focus{12, 1})), "0/37736447") + C.Check.str("masks: a hidden layer stays hidden, not dimmed", ms(L.Lyr.masks(0, 1, L.Focus{12, 1})), "1/37736446") + C.Check.str("masks: the active layer is never hidden", ms(L.Lyr.masks(0, 4096, L.Focus{12, 2})), "37736447/0") + C.Check.str("a click on a layer focuses it, the rest dim", fs(L.Lyr.pick(L.Lyr.plain(), 16)), "16|1") + C.Check.str("a click on another moves the focus", fs(L.Lyr.pick(L.Focus{16, 2}, 3)), "3|2") + C.Check.str("a click on it again puts all back", fs(L.Lyr.pick(L.Focus{16, 1}, 16)), "16|0") + C.Check.str("ctrl+H from nothing dims around front copper", fs(L.Lyr.cycle(0, L.Lyr.plain())), "12|1") + C.Check.str("then hides", fs(L.Lyr.cycle(0, L.Focus{12, 1})), "12|2") + C.Check.str("then back", fs(L.Lyr.cycle(0, L.Focus{12, 2})), "12|0") + C.Check.str("lifted: the others first", bool(L.Lyr.lift.ok(L.Lyr.lift.under(L.Focus{5, 1}), 12)) ++ bool(L.Lyr.lift.ok(L.Lyr.lift.under(L.Focus{5, 1}), 5)), "truefalse") + C.Check.str("then the active one alone", bool(L.Lyr.lift.ok(L.Lyr.lift.over(L.Focus{5, 1}), 5)) ++ bool(L.Lyr.lift.ok(L.Lyr.lift.over(L.Focus{5, 1}), 12)), "truefalse") + C.Check.str("nothing lifted: all", bool(L.Lyr.lift.ok(L.Lyr.lift.none(), 5)), "true") + C.Check.str("the web and phones draw the active one last", U32.show(L.Lyr.top(L.Focus{5, 1})) ++ " " ++ U32.show(L.Lyr.top(L.Focus{5, 0})), "5 255") + C.Check.str("the text round trip", fs(L.Lyr.read(L.Lyr.show(L.Focus{16, 2}))), "16|2") + C.Check.str("nothing read is no focus", fs(L.Lyr.read("")), "255|0") + C.Check.str("the kind word keeps the kind", U32.show(L.Lyr.kind.base(L.Lyr.kind.with(9, L.Focus{16, 2}))), "9") + C.Check.str("and the focus", fs(L.Lyr.kind.focus(L.Lyr.kind.with(9, L.Focus{16, 2}))), "16|2") diff --git a/test/native/viewer_bench.bend b/test/native/viewer_bench.bend index c9858eca62..8ce5f6719d 100644 --- a/test/native/viewer_bench.bend +++ b/test/native/viewer_bench.bend @@ -87,7 +87,7 @@ def with_hits(n: N.Nui, hs: List<&2, N.Hit>, +ms: Nat) -> N.Nui: # the frame's base brought up to date, as Shell.paint.k does def based(+n: N.Nui, +ms: Nat) -> N.Nui: - N.Nui.with_vw(n, MA.Shell.vw.if(N.Nui.panel(n), N.Nui.vw(n), N.Nui.width(n), N.Nui.height(n), N.Nui.vx0(n), ms, N.Nui.flip(n))) + N.Nui.with_vw(n, MA.Shell.vw.if(N.Nui.panel(n), N.Nui.vw(n), N.Nui.width(n), N.Nui.height(n), N.Nui.vx0(n), ms, N.Nui.flip(n), N.Nui.lfocus.bits(n))) def base_of(vw: V.Vw) -> Image: match vw: @@ -128,7 +128,7 @@ def Times.show(t: Times, +n: Nat) -> String: ++ " tiles drawn " ++ tenths(k, n) def same(+w: U32, +h: U32, +os: List<&2, R.Op>, +vw: V.Vw, +x0: U32, +ms: Nat, +flip: U32, +tiles: TI.Tiles) -> Bool: - TI.Eq.image(TI.Tiles.image(tiles), TI.Tiles.image(TI.Plan.run(MA.Shell.image(True{}, os, vw, w, h, x0, ms, flip, Nil{}, Map.new(&2, BM.Bitmap), TI.FNone{}, 0), TI.Frame.edge(w, h)))) + TI.Eq.image(TI.Tiles.image(tiles), TI.Tiles.image(TI.Plan.run(MA.Shell.image(True{}, os, vw, w, h, x0, ms, flip, 0, Nil{}, Map.new(&2, BM.Bitmap), TI.FNone{}, 0), TI.Frame.edge(w, h)))) # a frame's steps, timed type Laid is Data: @@ -159,7 +159,7 @@ type Fin is Data: def f.tiles.k(+n2: N.Nui, +o: L.Out, +memo: L.TLMemo, +oh: List<&2, N.Hit>, fr0: TI.Frames, +ms: Nat, +a: Nat, +b: Nat, +c: Nat, t: Times, ok: Bool, +check: Bool) -> IO(Fin): do IO: +t3 : Nat <- IO.now() - +pl : TI.Plan = MA.Shell.image(N.Nui.panel(n2), ops(o), N.Nui.vw(n2), N.Nui.width(n2), N.Nui.height(n2), N.Nui.vx0(n2), ms, N.Nui.flip(n2), hits(o), Map.new(&2, BM.Bitmap), fr0, N.Hits.slide(hits(o), oh)) + +pl : TI.Plan = MA.Shell.image(N.Nui.panel(n2), ops(o), N.Nui.vw(n2), N.Nui.width(n2), N.Nui.height(n2), N.Nui.vx0(n2), ms, N.Nui.flip(n2), N.Nui.lfocus.bits(n2), hits(o), Map.new(&2, BM.Bitmap), fr0, N.Hits.slide(hits(o), oh)) +kd : Nat = TI.Plan.count(pl) +t4 : Nat <- IO.now() +d : TI.Drawn = TI.Plan.drawn(pl, TI.Frame.edge(N.Nui.width(n2), N.Nui.height(n2))) @@ -231,7 +231,7 @@ def warm(+ty: L.Ty, +cats: Map<&2, CA.CatF>, +n0: N.Nui, +ms: Nat) -> Kept: +memo = L.Lay.memo(ty, n, L.TL.memo.none()) +o = BV.Bv.frame(ty, memo, cats, n) Kept{with_hits(n, hits(o), ms), memo, TI.FSome{N.Nui.width(n), N.Nui.height(n), - TI.Plan.run(MA.Shell.image(N.Nui.panel(n), ops(o), N.Nui.vw(n), N.Nui.width(n), N.Nui.height(n), N.Nui.vx0(n), ms, N.Nui.flip(n), hits(o), Map.new(&2, BM.Bitmap), TI.FNone{}, 0), TI.Frame.edge(N.Nui.width(n), N.Nui.height(n)))}} + TI.Plan.run(MA.Shell.image(N.Nui.panel(n), ops(o), N.Nui.vw(n), N.Nui.width(n), N.Nui.height(n), N.Nui.vx0(n), ms, N.Nui.flip(n), N.Nui.lfocus.bits(n), hits(o), Map.new(&2, BM.Bitmap), TI.FNone{}, 0), TI.Frame.edge(N.Nui.width(n), N.Nui.height(n)))}} def arg.u(+xs: List<&2, String>, +i: Nat, +d: U32) -> U32: F32.to_u32(Maybe.default(&2, F32, F32.read(Maybe.default(&2, String, List.get(&2, String, xs, i), "")), U32.to_f32(d))) @@ -252,7 +252,7 @@ def kept_n(k: Kept) -> N.Nui: def fresh.k(vw: V.Vw, +w: U32, +h: U32, +x0: U32, +ms: Nat, +flip: U32) -> Image: match vw: case V.Vw{+ss, +v, _, _, _, _, _, _, _, _, +vx}: - MA.Shell.base(w, h, x0, ss, v, ms, flip, False{}, vx) + MA.Shell.base(w, h, x0, ss, v, ms, flip, 0, False{}, vx) def fresh(+n: N.Nui, +ms: Nat) -> Image: fresh.k(N.Nui.vw(n), N.Nui.width(n), N.Nui.height(n), N.Nui.vx0(n), ms, N.Nui.flip(n))