diff --git a/macpal/Sources/FuserPlugin.swift b/macpal/Sources/FuserPlugin.swift index 4cad686d7..97d3a540c 100644 --- a/macpal/Sources/FuserPlugin.swift +++ b/macpal/Sources/FuserPlugin.swift @@ -350,7 +350,12 @@ final class FuserPlugin: NSObject, PalPlugin, WidthHinting { private var tick4 = 0 // git refresh every 4th tick (~4s) - init(home: String, repo: String) { + // minimal: hide every info row (status line, tasks, overtime, terminal pane) + // so the badge is just the avatar graphic + the name title. + let minimal: Bool + + init(home: String, repo: String, minimal: Bool = false) { + self.minimal = minimal self.home = home self.initRepo = repo // A persisted menu choice wins over the launch arg; otherwise track the @@ -364,7 +369,7 @@ final class FuserPlugin: NSObject, PalPlugin, WidthHinting { self.repoLabel = Self.knownRepos.first(where: { $0.path == repo })?.label ?? (repo as NSString).lastPathComponent } - self.hasPane = FileManager.default.fileExists(atPath: home + "/pane.log") + self.hasPane = !minimal && FileManager.default.fileExists(atPath: home + "/pane.log") super.init() } @@ -462,6 +467,7 @@ final class FuserPlugin: NSObject, PalPlugin, WidthHinting { // Reserved height from the y=12 baseline up to where the name sits — the // exact bottom-up stack the original badge computed. func stackHeight(in controller: PalController) -> CGFloat { + if minimal { return 0 } let off = (hasPane && curPaneH > 0) ? curPaneH + 8 : 0 let tasksH: CGFloat = taskLines.isEmpty ? 0 : CGFloat(taskLines.count) * 13 + 3 let chipH: CGFloat = overtimeOn ? 36 : 0 @@ -472,6 +478,12 @@ final class FuserPlugin: NSObject, PalPlugin, WidthHinting { } func layoutRows(in controller: PalController, originY: CGFloat) { + if minimal { + statusField.isHidden = true; tasksField.isHidden = true + overtimeField.isHidden = true; overtimeChip.isHidden = true + paneContainer?.isHidden = true + return + } let base = originY // 12 in practice (single plugin) let W = controller.fullWidth let off = (hasPane && curPaneH > 0) ? curPaneH + 8 : 0 @@ -503,6 +515,12 @@ final class FuserPlugin: NSObject, PalPlugin, WidthHinting { } func setCollapsed(_ collapsed: Bool) { + if minimal { + statusField.isHidden = true; tasksField.isHidden = true + overtimeField.isHidden = true; overtimeChip.isHidden = true + paneContainer?.isHidden = true + return + } let hide = collapsed statusField.isHidden = hide tasksField.isHidden = hide || taskLines.isEmpty diff --git a/macpal/Sources/Pal3D.swift b/macpal/Sources/Pal3D.swift index db75ee3da..632541922 100644 --- a/macpal/Sources/Pal3D.swift +++ b/macpal/Sources/Pal3D.swift @@ -146,7 +146,10 @@ final class Pal3DView: NSView { for clip in ["wave", "cheer"] { let cp = "\(dir)/\(stem)-\(clip).usdc" guard FileManager.default.fileExists(atPath: cp), - let n = loadUSD(cp, tint: tint, loop: false) else { continue } + // Clips (neo-wave/neo-cheer) carry no PBR sidecars of their + // own — reuse the idle model's textures (neo-base_color.png) + // so a hover/cheer swap stays textured, not raw-material green. + let n = loadUSD(cp, tint: tint, loop: false, textureStem: stem) else { continue } // Same rig as the idle → reuse the idle's exact fit transform so // swapping clips never zooms/shifts the camera framing. (Fitting // each independently differs because they load at different poses.) @@ -179,7 +182,8 @@ final class Pal3DView: NSView { // SceneKit reads UsdSkel animation natively; .playRepeatedly auto-starts and // loops every embedded clip. glb→usd conversion can drop textures, so the // sibling PBR maps are re-applied (same as the OBJ path). - private static func loadUSD(_ path: String, tint: NSColor, loop: Bool = true) -> SCNNode? { + private static func loadUSD(_ path: String, tint: NSColor, loop: Bool = true, + textureStem: String? = nil) -> SCNNode? { let url = URL(fileURLWithPath: path) let policy: SCNSceneSource.AnimationImportPolicy = loop ? .playRepeatedly : .doNotPlay guard let scene = try? SCNScene(url: url, options: [.animationImportPolicy: policy]) else { return nil } @@ -188,7 +192,9 @@ final class Pal3DView: NSView { if holder.childNodes.isEmpty { return nil } let dir = url.deletingLastPathComponent() - let stem = url.deletingPathExtension().lastPathComponent // e.g. "blueberry" / "neo" + // Clips pass `textureStem` to reuse the idle model's PBR maps (their own + // -base_color.png doesn't exist); the idle uses its own stem. + let stem = textureStem ?? url.deletingPathExtension().lastPathComponent // e.g. "blueberry" / "neo" let tintColor = accentTint(tint, 0.5) // pull the model toward the accent indigo func map(_ suffix: String) -> NSImage? { for n in ["\(stem)-\(suffix).png", "blueberry-\(suffix).png"] { diff --git a/macpal/Sources/PalCore.swift b/macpal/Sources/PalCore.swift index aaa7a925b..80ae43644 100644 --- a/macpal/Sources/PalCore.swift +++ b/macpal/Sources/PalCore.swift @@ -357,7 +357,7 @@ final class PalController: NSObject { // menu toggle can swap it live. The 3D pal gets a bigger frame so it reads. func makeGlyphView() -> NSView { if use3D, let path = config.model3DPath { - glyphW = 188; glyphH = 188 + glyphW = 224; glyphH = 224 let v = Pal3DView(frame: NSRect(x: (fullWidth - glyphW) / 2, y: 0, width: glyphW, height: glyphH), objPath: path, tint: accent) v.wantsLayer = true @@ -527,9 +527,9 @@ final class PalController: NSObject { items.append(gold); items.append(silver) } if config.avatarTogglable { - let m3 = NSMenuItem(title: "3D Blueberry", action: #selector(use3DAvatar), keyEquivalent: "") + let m3 = NSMenuItem(title: "3D", action: #selector(use3DAvatar), keyEquivalent: "") m3.target = self; m3.state = use3D ? .on : .off - let m2 = NSMenuItem(title: "2D Blueberry", action: #selector(use2DAvatar), keyEquivalent: "") + let m2 = NSMenuItem(title: "2D", action: #selector(use2DAvatar), keyEquivalent: "") m2.target = self; m2.state = use3D ? .off : .on items.append(m3); items.append(m2) } @@ -876,9 +876,14 @@ final class PalController: NSObject { let win = fullWidth // Stack, bottom-up: [plugin rows] · name · glyph. let stackH = plugins.reduce(CGFloat(0)) { $0 + $1.stackHeight(in: self) } - let nameY: CGFloat = 12 + stackH - let glyphY = nameY + 44 + 4 - let totalH = glyphY + glyphH + 8 + let nameY: CGFloat = 8 + stackH + let nameH: CGFloat = 40 + // Tuck the title right under the avatar. The name is drawn centered (and + // doesn't clip), and the avatar art has whitespace at its base, so a tight + // — even slightly negative — gap reads as "graphic sitting on the title." + // 3D has extra frame headroom, so it tucks further. + let glyphY = nameY + nameH + (use3D ? -40 : -4) + let totalH = glyphY + glyphH + 6 var o = NSPoint(x: f.minX + marginX, y: f.maxY - totalH - marginY) // TL switch corner { case "TR": o = NSPoint(x: f.maxX - win - marginX, y: f.maxY - totalH - marginY) @@ -890,7 +895,7 @@ final class PalController: NSObject { content.frame = NSRect(x: 0, y: 0, width: win, height: totalH) let gw: CGFloat = (glyphView is NSImageView) ? glyphW : win glyphView.frame = NSRect(x: (win - gw) / 2, y: glyphY, width: gw, height: glyphH) - nameLabel.frame = NSRect(x: 0, y: nameY, width: win, height: 44) + nameLabel.frame = NSRect(x: 0, y: nameY, width: win, height: nameH) clickCatcher.frame = glyphView.frame content.liveRects = [glyphView.frame] // Plugins lay their rows out bottom-up from y=12 and append any live rects. diff --git a/macpal/Sources/main.swift b/macpal/Sources/main.swift index 13c62d9fd..727d1df82 100644 --- a/macpal/Sources/main.swift +++ b/macpal/Sources/main.swift @@ -57,7 +57,7 @@ if profile == "fuser" { noteSignalDir: noteDir, factoryCorner: corner, fixedCorner: corner, - marginX: 16, marginY: 16, // the badge's historical resting gap + marginX: 4, marginY: 4, // hug the screen corner draggable: false, colorTogglable: false, registersLoginItem: false, @@ -68,7 +68,7 @@ if profile == "fuser" { }, singPath: { _ in fuserHome + "/glyph-sing.svg" } ) - plugins = [FuserPlugin(home: fuserHome, repo: repo)] + plugins = [FuserPlugin(home: fuserHome, repo: repo, minimal: argv.contains("--minimal"))] } else { // The star, for Fía. Glyph art ships in the .app's Resources (gold/silver). func base(_ color: String) -> String { color == "silver" ? "star-silver" : "star-glyph" } @@ -105,9 +105,13 @@ if profile == "fuser" { config.model3D = true config.model3DPath = modelPath config.avatarTogglable = true // right-click → 3D ⇄ 2D - // 2D mode shows the model's flat thumbnail (no SVG glyph for these). - config.posePaths = { _ in [fuserHome + "/\(m)-3d/\(m)-thumb.png"] } - config.singPath = { _ in nil } + // 2D keeps the flat SVG glyph poses (set by the fuser config above) so + // the toggle flips between the SceneKit pal and the animated glyph — + // only fall back to the model's static thumbnail if no SVGs are staged. + if !FileManager.default.fileExists(atPath: fuserHome + "/glyph.svg") { + config.posePaths = { _ in [fuserHome + "/\(m)-3d/\(m)-thumb.png"] } + config.singPath = { _ in nil } + } } }