From 04a081341371ca15f6b9502de95820d484e32a75 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 15:57:25 +0000 Subject: [PATCH 1/3] Build the contraption builder UI once instead of on every open The builder regenerated its entire ~200-control chrome on every preOpenCard (startCB -> buildUI -> clearUI + recreate). Guard the build so it runs once and persists with the saved stack: - chromeBuilt() reports whether the palette group exists AND a stamped uUIVersion custom property matches kUIVersion. startCB builds + stamps only when it doesn't, so reopening a saved stack skips the rebuild entirely. - Only the native physics world is recreated each open (it can't be serialised); layoutPalette re-applies accordion state to the persisted UI. - prepArena reuses the arena graphics instead of duplicating them on reopen. - rebuildCB forces a one-time regenerate after editing the builder; bumping kUIVersion makes any older saved stack rebuild itself once. https://claude.ai/code/session_01XpBcQg2DbncrBcFZLHqhMj --- ...box2dxt-contraption-builder.livecodescript | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/examples/box2dxt-contraption-builder.livecodescript b/examples/box2dxt-contraption-builder.livecodescript index 77920df..296f9d2 100644 --- a/examples/box2dxt-contraption-builder.livecodescript +++ b/examples/box2dxt-contraption-builder.livecodescript @@ -1346,6 +1346,8 @@ constant kPaletteW = 196, kInspectorW = 248, kStatusH = 26 constant kCanvasInset = 2, kGroundBarH = 22 constant kPalPadX = 12, kPalBtnH = 28, kPalBtnGap = 4 constant kPalSectionGap = 8, kPalHeaderH = 22, kPalScrollW = 16, kPalGutterW = 22 +-- Bump kUIVersion whenever the built chrome changes, so older saved stacks rebuild once. +constant kUIVersion = "1" -- Palette theme: one source of truth for the chrome colours (RGB strings) so the -- idle / active / hover styling never drifts apart. Used by the setBtn*, highlight* @@ -1383,6 +1385,11 @@ constant kEntryKeys = "width,height,length,thickness,diameter,size,angle,bounce, -- ===================================================================== -- Lifecycle -- ===================================================================== +-- Build once: startCB generates the chrome on first run and stamps a version on the +-- stack, so save the stack afterwards. On later opens chromeBuilt() is true, so the +-- ~200-control UI is NOT rebuilt — only the native physics world is recreated (it +-- can't be serialised). Run rebuildCB to regenerate the chrome after editing the +-- builder; bumping kUIVersion makes any older saved stack rebuild itself once. on preOpenCard startCB end preOpenCard @@ -1422,13 +1429,17 @@ on startCB resetSceneState resetJointArrays resetPend - buildUI + if not chromeBuilt() then -- build the chrome once; it persists in the saved stack + buildUI + set the uUIVersion of this stack to kUIVersion + end if b2kSetup 0, kDefaultGravity -- creates the world; the loop stays stopped prepArena b2kFrameTarget the long id of this stack b2kContactTarget the long id of this stack raiseUI highlightAll + layoutPalette -- re-apply accordion state to the persisted palette showInspectorForTool put "Welcome! Pick a shape on the left, click the stage to drop it, then press Run." into gStatus updateHud @@ -1444,6 +1455,12 @@ on stopCB clearSceneControls end stopCB +-- Force a one-time chrome rebuild — after editing the builder, or to repair the UI. +on rebuildCB + set the uUIVersion of this stack to empty + startCB +end rebuildCB + -- Draw every part + joint once. Build mode stops the loop (so nothing falls), -- which also stops the Kit's own redraw — so we sync by hand after each edit. on renderBuild @@ -1469,7 +1486,7 @@ on prepArena put tCanvasR - kCanvasInset into gArenaR put tCanvasT + kCanvasInset into gArenaT put tCanvasB - kGroundBarH into gArenaB - create graphic "cb_field" + if there is not a graphic "cb_field" then create graphic "cb_field" set the style of graphic "cb_field" to "rectangle" set the filled of graphic "cb_field" to true set the backgroundColor of graphic "cb_field" to "26,29,37" @@ -1480,7 +1497,7 @@ on prepArena b2kWall gArenaL, gArenaT, gArenaR, gArenaT -- ceiling b2kWall gArenaL, gArenaB, gArenaL, gArenaT -- left wall b2kWall gArenaR, gArenaB, gArenaR, gArenaT -- right wall - create graphic "cb_groundbar" + if there is not a graphic "cb_groundbar" then create graphic "cb_groundbar" set the style of graphic "cb_groundbar" to "rectangle" set the filled of graphic "cb_groundbar" to true set the backgroundColor of graphic "cb_groundbar" to "44,49,60" @@ -1491,6 +1508,13 @@ end prepArena -- ===================================================================== -- UI -- ===================================================================== +-- The chrome is generated once and saved with the stack; chromeBuilt reports whether +-- the persisted UI is present and current, so startCB can skip the rebuild on reopen. +-- (A version mismatch — e.g. after the builder is updated — rebuilds it once.) +function chromeBuilt + return (there is a group "ui_palettescroll") and (the uUIVersion of this stack is kUIVersion) +end chromeBuilt + -- Build the whole UI: a top action bar, a grouped tool palette on the left, an -- inspector/help panel on the right, and a status bar along the bottom. on buildUI From 4ac17fa4d3f354045d44d035e19d20b1661b00f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 16:12:59 +0000 Subject: [PATCH 2/3] Re-categorize and colour-code the palette; fix the scrollbar resize bug Address three issues with the left palette: - Re-categorize: Drag/Delete/Duplicate/Multiply move out of SHAPES into a new TOOLS category; SHAPES now holds only droppable parts (Box/Ball/Capsule/ Polygon/Image/Anchor). Six categories: TOOLS, SHAPES, SPECIAL, TERRAIN, VEHICLES, JOINTS. - Fix the "resizes weirdly" scrollbar: collapsed rows were left at stale positions, so the group's formattedHeight (and thus the native scrollbar thumb/range) was wrong. layoutPalette now parks collapsed rows at the top of the content so the scroll extent tracks only visible rows. The accordion is also single-open now (opening one category collapses the others), so the toolbox always fits the window and the scrollbar stays hidden. - Make it pop: each category has an accent colour (categoryColor) that fills its header when open, tints the title when collapsed, and lights up the selected tool inside it, so the palette reads as distinct, colour-coded groups. kUIVersion bumped to 2 so existing built stacks pick up the new layout once. https://claude.ai/code/session_01XpBcQg2DbncrBcFZLHqhMj --- ...box2dxt-contraption-builder.livecodescript | 119 +++++++++++++----- 1 file changed, 86 insertions(+), 33 deletions(-) diff --git a/examples/box2dxt-contraption-builder.livecodescript b/examples/box2dxt-contraption-builder.livecodescript index 296f9d2..7421dce 100644 --- a/examples/box2dxt-contraption-builder.livecodescript +++ b/examples/box2dxt-contraption-builder.livecodescript @@ -1326,8 +1326,10 @@ local gGravityMode -- world gravity preset: normal|low|zero| local gPreRun -- snapshot of the build, taken when Run starts local gPalOpen -- CR-list of palette section titles currently expanded -constant kShapeTools = "drag,box,ball,capsule,poly,image,anchor,delete,duplicate,multiply" -constant kShapeLabels = "Drag,Box,Ball,Capsule,Poly,Image,Anchor,Delete,Duplicate,Multiply" +constant kEditTools = "drag,delete,duplicate,multiply" +constant kEditLabels = "Drag,Delete,Duplicate,Multiply" +constant kShapeTools = "box,ball,capsule,poly,image,anchor" +constant kShapeLabels = "Box,Ball,Capsule,Polygon,Image,Anchor" constant kSpecialTools = "balloon,bomb,plate,fan,magnet" constant kSpecialLabels = "Balloon,Bomb,Pressure Plate,Fan,Magnet" constant kTerrainTools = "platform,ramp,hill" @@ -1347,7 +1349,7 @@ constant kCanvasInset = 2, kGroundBarH = 22 constant kPalPadX = 12, kPalBtnH = 28, kPalBtnGap = 4 constant kPalSectionGap = 8, kPalHeaderH = 22, kPalScrollW = 16, kPalGutterW = 22 -- Bump kUIVersion whenever the built chrome changes, so older saved stacks rebuild once. -constant kUIVersion = "1" +constant kUIVersion = "2" -- Palette theme: one source of truth for the chrome colours (RGB strings) so the -- idle / active / hover styling never drifts apart. Used by the setBtn*, highlight* @@ -1425,7 +1427,7 @@ on startCB put "normal" into gGravityMode put empty into gHoverId put 0 into gStressCount - put "SHAPES" & cr & "TERRAIN" & cr & "SPECIAL" & cr & "VEHICLES" & cr & "JOINTS" into gPalOpen + put "SHAPES" into gPalOpen -- single-open accordion: SHAPES is the category open first resetSceneState resetJointArrays resetPend @@ -1551,18 +1553,20 @@ on makeTopBar end repeat end makeTopBar --- Left palette: SHAPES, TERRAIN, SPECIAL, VEHICLES and JOINTS, each under a --- section header. The sections are wrapped in a vertically scrolling group so the --- whole tool list fits a normal-height window — the panel scrolls, not the stack. +-- Left palette: six colour-coded categories (TOOLS, SHAPES, SPECIAL, TERRAIN, +-- VEHICLES, JOINTS) as a single-open accordion — opening one collapses the others, +-- so the whole toolbox fits the window without a scrollbar. Drag/Delete/Duplicate/ +-- Multiply live under TOOLS; SHAPES holds only the parts you actually drop. on makePalette local tWide, tHigh, tY put the width of this card into tWide put the height of this card into tHigh makeBar "ui_palettebg", 0, kTopBarH + kAccentH, kPaletteW, tHigh - kStatusH, kColPaletteBg put kTopBarH + kAccentH + 8 into tY + put makeToolSection("TOOLS", kEditTools, kEditLabels, "tool", tY) into tY put makeToolSection("SHAPES", kShapeTools, kShapeLabels, "tool", tY) into tY - put makeToolSection("TERRAIN", kTerrainTools, kTerrainLabels, "tool", tY) into tY put makeToolSection("SPECIAL", kSpecialTools, kSpecialLabels, "tool", tY) into tY + put makeToolSection("TERRAIN", kTerrainTools, kTerrainLabels, "tool", tY) into tY put makeToolSection("VEHICLES", kVehicleTools, kVehicleLabels, "tool", tY) into tY put makeToolSection("JOINTS", kJointTools, kJointLabels, "joint", tY) into tY groupPalette @@ -1578,17 +1582,17 @@ on groupPalette -- palette rather than aborting the whole UI build. try select empty - repeat for each item tT in "SHAPES,TERRAIN,SPECIAL,VEHICLES,JOINTS" + repeat for each item tT in "TOOLS,SHAPES,SPECIAL,TERRAIN,VEHICLES,JOINTS" if there is a button ("ui_hdr_" & tT) then set the selected of button ("ui_hdr_" & tT) to true if there is a graphic ("ui_hdrrule_" & tT) then set the selected of graphic ("ui_hdrrule_" & tT) to true end repeat - repeat for each item tId in (kShapeTools & comma & kTerrainTools & comma & kSpecialTools & comma & kVehicleTools) + repeat for each item tId in (kEditTools & comma & kShapeTools & comma & kTerrainTools & comma & kSpecialTools & comma & kVehicleTools) if there is a button ("ui_tool_" & tId) then set the selected of button ("ui_tool_" & tId) to true end repeat repeat for each item tId in kJointTools if there is a button ("ui_joint_" & tId) then set the selected of button ("ui_joint_" & tId) to true end repeat - repeat for each item tId in (kShapeTools & comma & kTerrainTools & comma & kSpecialTools & comma & kVehicleTools & comma & kJointTools) + repeat for each item tId in (kEditTools & comma & kShapeTools & comma & kTerrainTools & comma & kSpecialTools & comma & kVehicleTools & comma & kJointTools) if there is a field ("ui_g_" & tId) then set the selected of field ("ui_g_" & tId) to true end repeat group @@ -1659,6 +1663,8 @@ end chevronGlyph -- Title → its tool ids / kind. No new data: routes to the existing constant lists. function sectionIds pTitle switch pTitle + case "TOOLS" + return kEditTools case "SHAPES" return kShapeTools case "TERRAIN" @@ -1678,48 +1684,91 @@ function sectionKind pTitle return "tool" end sectionKind --- Lay the palette out from gPalOpen: headers always show (with a chevron); an --- expanded section reveals its rule + glyph gutters + tool buttons, a collapsed one --- hides them. Hidden controls leave the group's scroll extent, so the scrollbar only --- appears when the *visible* content overflows the viewport. +-- Each category owns an accent colour. It fills the header when the section is open, +-- tints the header title when collapsed, and lights up the selected tool inside it — +-- so the palette reads as distinct, colour-coded groups (white text stays legible on +-- every one of these). +function categoryColor pTitle + switch pTitle + case "TOOLS" + return "76,122,210" + case "SHAPES" + return kColAccentGreen + case "SPECIAL" + return "158,100,205" + case "TERRAIN" + return "190,140,72" + case "VEHICLES" + return "66,172,182" + case "JOINTS" + return kColAccentAmber + end switch + return kColAccentGreen +end categoryColor + +-- Lay the palette out from gPalOpen. Every header shows (chevron + colour-coded +-- title); the open category reveals its rule + glyph gutters + tool buttons, while +-- collapsed categories hide their rows AND park them at the top of the content so +-- they add no height to the group's scroll extent (that stale-rect inflation was +-- what made the scrollbar resize oddly). With one category open at a time the +-- content fits the viewport, so the scrollbar normally stays hidden. on layoutPalette if there is not a group "ui_palettescroll" then exit layoutPalette - local tY, tTitle, tIds, tPrefix, tId, tBtn, tGlyph, tOpen, tRight, tGut + local tY, tTitle, tIds, tPrefix, tId, tBtn, tGlyph, tOpen, tRight, tGut, tPark, tHdr lock screen set the vScroll of group "ui_palettescroll" to 0 -- lay children out in unscrolled card coords put kPaletteW - kPalPadX - kPalScrollW into tRight put kPalPadX + kPalGutterW into tGut put kTopBarH + kAccentH + 8 into tY - repeat for each item tTitle in "SHAPES,TERRAIN,SPECIAL,VEHICLES,JOINTS" + put tY into tPark -- collapsed rows park here (top of content) + repeat for each item tTitle in "TOOLS,SHAPES,SPECIAL,TERRAIN,VEHICLES,JOINTS" put sectionIds(tTitle) into tIds if sectionKind(tTitle) is "joint" then put "ui_joint_" into tPrefix else put "ui_tool_" into tPrefix - set the rect of button ("ui_hdr_" & tTitle) to kPalPadX, tY, tRight, tY + kPalHeaderH - set the label of button ("ui_hdr_" & tTitle) to (chevronGlyph(tTitle) && tTitle) + put ("ui_hdr_" & tTitle) into tHdr put (tTitle is among the lines of gPalOpen) into tOpen + -- Header reads as a colour-coded bar: filled in the category colour when open, + -- just the coloured title when collapsed. + set the rect of button tHdr to kPalPadX, tY, tRight, tY + kPalHeaderH + set the label of button tHdr to (chevronGlyph(tTitle) && tTitle) + if tOpen then + set the opaque of button tHdr to true + set the backgroundColor of button tHdr to categoryColor(tTitle) + set the textColor of button tHdr to kColBtnText + else + set the opaque of button tHdr to false + set the textColor of button tHdr to categoryColor(tTitle) + end if add kPalHeaderH to tY if tOpen then - set the visible of graphic ("ui_hdrrule_" & tTitle) to true set the rect of graphic ("ui_hdrrule_" & tTitle) to kPalPadX, tY, tRight, tY + 1 + set the visible of graphic ("ui_hdrrule_" & tTitle) to true add 6 to tY repeat for each item tId in tIds put tPrefix & tId into tBtn put "ui_g_" & tId into tGlyph if there is a button tBtn then - set the visible of button tBtn to true set the rect of button tBtn to tGut, tY, tRight, tY + kPalBtnH + set the visible of button tBtn to true end if if there is a field tGlyph then - set the visible of field tGlyph to true set the rect of field tGlyph to kPalPadX, tY, tGut, tY + kPalBtnH + set the visible of field tGlyph to true end if add kPalBtnH + kPalBtnGap to tY end repeat else + set the rect of graphic ("ui_hdrrule_" & tTitle) to kPalPadX, tPark, tRight, tPark + 1 set the visible of graphic ("ui_hdrrule_" & tTitle) to false repeat for each item tId in tIds - if there is a button (tPrefix & tId) then set the visible of button (tPrefix & tId) to false - if there is a field ("ui_g_" & tId) then set the visible of field ("ui_g_" & tId) to false + if there is a button (tPrefix & tId) then + set the rect of button (tPrefix & tId) to tGut, tPark, tRight, tPark + kPalBtnH + set the visible of button (tPrefix & tId) to false + end if + if there is a field ("ui_g_" & tId) then + set the rect of field ("ui_g_" & tId) to kPalPadX, tPark, tGut, tPark + kPalBtnH + set the visible of field ("ui_g_" & tId) to false + end if end repeat end if add kPalSectionGap to tY @@ -1728,17 +1777,15 @@ on layoutPalette unlock screen end layoutPalette --- Collapse or expand a section, then relayout. Pure chrome: tool selection and the --- HUD are deliberately left untouched (gPalOpen never carries a trailing newline, so --- the delete/append idiom below stays clean across many toggles). +-- Single-open accordion: opening a category collapses the others, so the toolbox +-- always fits without a scrollbar. Pure chrome — tool selection and the HUD are left +-- untouched. gPalOpen holds at most one category title (empty = all collapsed). on togglePaletteSection pTitle if pTitle is empty then exit togglePaletteSection if pTitle is among the lines of gPalOpen then - delete line lineOffset(pTitle, gPalOpen) of gPalOpen - else if gPalOpen is empty then - put pTitle into gPalOpen + put empty into gPalOpen -- click the open category to close it else - put pTitle into line (the number of lines of gPalOpen + 1) of gPalOpen + put pTitle into gPalOpen -- this category becomes the only open one end if layoutPalette end togglePaletteSection @@ -1923,9 +1970,15 @@ on setBtnActive pBtn, pColor set the textColor of button pBtn to kColBtnText end setBtnActive +-- Repaint every palette button. Each category's selected tool lights up in that +-- category's accent colour (Delete stays red); joints use the JOINTS accent. on highlightAll - highlightGroup "ui_tool_", (kShapeTools & "," & kTerrainTools & "," & kSpecialTools & "," & kVehicleTools), gTool, (gJointTool is empty), kColAccentGreen - highlightGroup "ui_joint_", kJointTools, gJointTool, true, kColAccentAmber + highlightGroup "ui_tool_", kEditTools, gTool, (gJointTool is empty), categoryColor("TOOLS") + highlightGroup "ui_tool_", kShapeTools, gTool, (gJointTool is empty), categoryColor("SHAPES") + highlightGroup "ui_tool_", kSpecialTools, gTool, (gJointTool is empty), categoryColor("SPECIAL") + highlightGroup "ui_tool_", kTerrainTools, gTool, (gJointTool is empty), categoryColor("TERRAIN") + highlightGroup "ui_tool_", kVehicleTools, gTool, (gJointTool is empty), categoryColor("VEHICLES") + highlightGroup "ui_joint_", kJointTools, gJointTool, true, categoryColor("JOINTS") highlightActions end highlightAll From ad1f6a4f156d814a1c711180cae6ced1f56ea587 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 16:23:30 +0000 Subject: [PATCH 3/3] Remove the palette's native scrollbar; re-assert its viewport rect The single-open accordion always fits the panel, but the native group scrollbar could still flicker/resize: with build-once the group's rect was set just once, so a stale height let the bar appear for taller categories and vanish for shorter ones as you switched between them. - layoutPalette re-asserts the group's viewport rect every layout (guards against a stale persisted height) and forces vScrollbar off; groupPalette builds it bar-free as well. - Dropped the content-height conditional that toggled the bar, and reclaimed the 16px scrollbar gutter so the palette padding is symmetric. kUIVersion -> 3 so existing built stacks pick up the change once. https://claude.ai/code/session_01XpBcQg2DbncrBcFZLHqhMj --- ...box2dxt-contraption-builder.livecodescript | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/examples/box2dxt-contraption-builder.livecodescript b/examples/box2dxt-contraption-builder.livecodescript index 7421dce..ac0a189 100644 --- a/examples/box2dxt-contraption-builder.livecodescript +++ b/examples/box2dxt-contraption-builder.livecodescript @@ -1349,7 +1349,7 @@ constant kCanvasInset = 2, kGroundBarH = 22 constant kPalPadX = 12, kPalBtnH = 28, kPalBtnGap = 4 constant kPalSectionGap = 8, kPalHeaderH = 22, kPalScrollW = 16, kPalGutterW = 22 -- Bump kUIVersion whenever the built chrome changes, so older saved stacks rebuild once. -constant kUIVersion = "2" +constant kUIVersion = "3" -- Palette theme: one source of truth for the chrome colours (RGB strings) so the -- idle / active / hover styling never drifts apart. Used by the setBtn*, highlight* @@ -1604,7 +1604,7 @@ on groupPalette set the margins of group "ui_palettescroll" to 0 set the opaque of group "ui_palettescroll" to false set the hScrollbar of group "ui_palettescroll" to false - set the vScrollbar of group "ui_palettescroll" to true + set the vScrollbar of group "ui_palettescroll" to false -- single-open always fits; no native bar set the unboundedVScroll of group "ui_palettescroll" to false set the rect of group "ui_palettescroll" to 0, kTopBarH + kAccentH, kPaletteW, (the height of this card) - kStatusH set the vScroll of group "ui_palettescroll" to 0 @@ -1620,7 +1620,7 @@ end groupPalette function makeToolSection pTitle, pIds, pLabels, pKind, pStartY local tY, tI, tId, tLbl, tName, tRight, tGut put pStartY into tY - put kPaletteW - kPalPadX - kPalScrollW into tRight -- shared right edge (leaves scrollbar room) + put kPaletteW - kPalPadX into tRight -- shared right edge (symmetric inset, no scrollbar) put kPalPadX + kPalGutterW into tGut -- labels start after the glyph gutter makeButton ("ui_hdr_" & pTitle), (chevronGlyph(pTitle) && pTitle), kPalPadX, tY, tRight - kPalPadX, kPalHeaderH set the textAlign of button ("ui_hdr_" & pTitle) to "left" @@ -1708,16 +1708,20 @@ end categoryColor -- Lay the palette out from gPalOpen. Every header shows (chevron + colour-coded -- title); the open category reveals its rule + glyph gutters + tool buttons, while --- collapsed categories hide their rows AND park them at the top of the content so --- they add no height to the group's scroll extent (that stale-rect inflation was --- what made the scrollbar resize oddly). With one category open at a time the --- content fits the viewport, so the scrollbar normally stays hidden. +-- collapsed categories hide their rows and park them at the top of the content. With +-- one category open at a time the whole toolbox fits the panel, so there is no native +-- scrollbar to flicker or resize — the viewport rect is re-asserted here each time. on layoutPalette if there is not a group "ui_palettescroll" then exit layoutPalette local tY, tTitle, tIds, tPrefix, tId, tBtn, tGlyph, tOpen, tRight, tGut, tPark, tHdr lock screen + -- Re-assert the viewport rect every layout (a stale persisted height was what let + -- the native bar flicker in) and keep it bar-free: a single open category always + -- fits, so nothing needs to scroll. + set the rect of group "ui_palettescroll" to 0, kTopBarH + kAccentH, kPaletteW, (the height of this card) - kStatusH + set the vScrollbar of group "ui_palettescroll" to false set the vScroll of group "ui_palettescroll" to 0 -- lay children out in unscrolled card coords - put kPaletteW - kPalPadX - kPalScrollW into tRight + put kPaletteW - kPalPadX into tRight put kPalPadX + kPalGutterW into tGut put kTopBarH + kAccentH + 8 into tY put tY into tPark -- collapsed rows park here (top of content) @@ -1773,7 +1777,6 @@ on layoutPalette end if add kPalSectionGap to tY end repeat - set the vScrollbar of group "ui_palettescroll" to ((tY - (kTopBarH + kAccentH)) > the height of group "ui_palettescroll") unlock screen end layoutPalette