diff --git a/CHANGELOG.md b/CHANGELOG.md index 370cd5e..19149db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,30 @@ The native shim's ABI is tracked separately by `b2Version()` (currently `2`). ## [Unreleased] +### Added + +- **Terrain objects in the Contraption Builder.** A new **TERRAIN** palette + section adds three pieces of static scenery the dynamic parts rest on, roll + down and bump into: **Platform** (a solid ledge), **Ramp** (a wedge for + rolling and sliding) and **Hill** (a rounded mound). Each is a static polygon + body, so it never falls and the Kit's sync skips it. They size, rotate (the + angle is baked into the outline so a static body tilts without a redraw it + never gets), recolour, take an adjustable grip (friction), drag, duplicate + and save/load like any other part — and joints can pin to them, so you can + hinge a part to a platform or hang a bridge from a ledge. +- **"Terrain Run" example recipe** — a ball rolls down a ramp, over a hill and + onto a platform, showing the new pieces in one click. + ### Fixed +- **Bridge & Chain spans ignore Bouncy mode.** Building a span while the + **Bouncy** build option was on made every plank/link springy, so bridges and + chains jittered and never settled. Span segments are now always built + non-bouncy, regardless of the toggle, so they hang and sag predictably. +- **Duplicating a rotated part keeps its size.** Duplicate copied the graphic's + *bounding box* (which inflates as a part rotates) as the new part's size, so a + rotated box, polygon, plank or terrain piece grew each time it was copied. It + now copies the stored design size instead. - **Kit no longer leaks image-angle cache entries across teardowns.** `b2kResetTables` now also clears the `sImgAngle` table, so repeatedly tearing a world down and rebuilding it (e.g. switching demo scene tabs) stops diff --git a/README.md b/README.md index 60ba4ce..6fe79ae 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ That's a full, draggable, gravity-driven physics scene — see | **Extension** | `src/box2dxt.lcb` | The xTalk Builder (LCB) library: `foreign handler` bindings wrapped in friendly `b2…` handlers. | | **The Kit** | `src/box2dxt-kit.livecodescript` | A batteries-included, pure-xTalk helper (`b2k…`) that works in pixels/degrees and hides the loop. | | **Demo** | `examples/box2dxt-demo.livecodescript` | A self-building, multi-scene testbed showing every feature. | -| **Contraption Builder** | `examples/box2dxt-contraption-builder.livecodescript` | An interactive GUI: drop parts (incl. images), wire up joints + motors, then Run. | +| **Contraption Builder** | `examples/box2dxt-contraption-builder.livecodescript` | An interactive GUI: drop parts (incl. images) and terrain, wire up joints + motors, then Run. | | **Prebuilt binaries** | `prebuilt/` | Drop-in native libraries so you can run without a toolchain. | There are two layers you can call: diff --git a/examples/box2dxt-contraption-builder.livecodescript b/examples/box2dxt-contraption-builder.livecodescript index ffd045c..8a53fce 100644 --- a/examples/box2dxt-contraption-builder.livecodescript +++ b/examples/box2dxt-contraption-builder.livecodescript @@ -1329,13 +1329,15 @@ constant kShapeTools = "drag,box,ball,capsule,poly,image,anchor,delete,duplica constant kShapeLabels = "Drag,Box,Ball,Capsule,Poly,Image,Anchor,Delete,Duplicate" constant kSpecialTools = "balloon,bomb,plate,fan,magnet" constant kSpecialLabels = "Balloon,Bomb,Pressure Plate,Fan,Magnet" +constant kTerrainTools = "platform,ramp,hill" +constant kTerrainLabels = "Platform,Ramp,Hill" constant kJointTools = "hinge,weld,rope,slider,wheel,bridge,chain" constant kJointLabels = "Hinge,Weld,Rope,Slider,Wheel,Bridge,Chain" constant kHingeTorque = 1500, kWheelTorque = 320 -- Layout, in screen pixels. Derived edges (canvas L/R/T/B) are computed from the -- card size in prepArena/buildUI, because LiveCode constants cannot be expressions. -constant kStackW = 1180, kStackH = 760 +constant kStackW = 1180, kStackH = 868 constant kTopBarH = 54, kAccentH = 3 constant kPaletteW = 196, kInspectorW = 248, kStatusH = 26 constant kCanvasInset = 2, kGroundBarH = 22 @@ -1358,6 +1360,8 @@ constant kFanW = 130, kFanH = 150, kFanForce = 900 constant kMagnetRange = 220, kMagnetForce = 1400, kMagnetMinDist = 16 constant kStressBatch = 50 constant kSizeMin = 16, kSizeMax = 240 +-- Terrain (static scenery) can be larger than parts — wide ledges, long ramps. +constant kTerrainMin = 20, kTerrainMax = 600 -- inspector: rows shown per tab, dimension nudge (px), joint click radius (px) constant kPropRows = 10, kDimStep = 8, kJointHitR = 16 -- numeric settings a user may type an exact value for (toggles/colour stay stepper-only) @@ -1517,6 +1521,7 @@ on makePalette makeBar "ui_palettebg", 0, kTopBarH + kAccentH, kPaletteW, tHigh - kStatusH, "26,28,35" put kTopBarH + kAccentH + 8 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("JOINTS", kJointTools, kJointLabels, "joint", tY) into tY end makePalette @@ -1704,7 +1709,7 @@ on makeButton pName, pLabel, pX, pY, pW, pH end makeButton on highlightAll - highlightGroup "ui_tool_", (kShapeTools & "," & kSpecialTools), gTool, (gJointTool is empty), "72,190,130" + highlightGroup "ui_tool_", (kShapeTools & "," & kTerrainTools & "," & kSpecialTools), gTool, (gJointTool is empty), "72,190,130" highlightGroup "ui_joint_", kJointTools, gJointTool, true, "232,150,72" highlightActions end highlightAll @@ -2001,6 +2006,10 @@ on handleAction pId hideRecipesMenu buildRecipeDominoes break + case "terrainrun" + hideRecipesMenu + buildRecipeTerrainRun + break case "stress" hideRecipesMenu stressTest @@ -2085,6 +2094,11 @@ function placePart pX, pY case "anchor" put placeAnchor(pX, pY) into tCtrl break + case "platform" + case "ramp" + case "hill" + put placeTerrain(gTool, pX, pY) into tCtrl + break case "image" put importImage(pX, pY) into tCtrl break @@ -2178,6 +2192,124 @@ function placeAnchor pX, pY return tRef end placeAnchor +-- ===================================================================== +-- Terrain: static scenery the dynamic parts rest on, roll down and bump +-- into. Each piece is a convex polygon graphic attached as a *static* +-- polygon body, so the Kit's sync skips it (it never moves) and we draw it +-- once. Rotation is baked into the points (the body angle stays 0), so a +-- ramp can tilt without the per-frame redraw a static body never receives. +-- ===================================================================== + +-- The static scenery kinds, handled together wherever they differ from parts. +function kindIsTerrain pKind + return pKind is among the items of "platform,ramp,hill" +end kindIsTerrain + +-- Default unrotated size for a fresh terrain piece, as "w,h" in pixels. +function terrainDefault pKind + switch pKind + case "ramp" + return "130,82" + case "hill" + return "172,84" + end switch + return "150,26" -- platform: a wide, shallow ledge +end terrainDefault + +-- A terrain piece's earthy default fill colour. +function terrainColor pKind + switch pKind + case "ramp" + return "150,120,84" -- packed dirt + case "hill" + return "96,132,86" -- grassy mound + end switch + return "108,116,128" -- platform: stone +end terrainColor + +-- Screen-space points for a terrain outline: the canonical shape scaled to +-- pW x pH with its bounding box centred on (pCx,pCy), then rotated pAngleDeg +-- about that centre. Convex and <= 8 verts, so b2kAddPolygon accepts it. +function terrainPoints pKind, pCx, pCy, pW, pH, pAngleDeg + local tHw, tHh, tBase, tPt, tx, ty, tC, tS, tA, tRot + set the itemDelimiter to comma -- we parse "x,y" points below + put pW / 2 into tHw + put pH / 2 into tHh + switch pKind + case "ramp" + -- right triangle: flat base, upright right edge, slope rising rightward + put (pCx - tHw) & "," & (pCy + tHh) & cr & (pCx + tHw) & "," & (pCy + tHh) \ + & cr & (pCx + tHw) & "," & (pCy - tHh) into tBase + break + case "hill" + -- a convex dome: flat base with an arched top + put (pCx - tHw) & "," & (pCy + tHh) & cr & (pCx + tHw) & "," & (pCy + tHh) & cr \ + & round(pCx + tHw * 0.62) & "," & round(pCy - tHh * 0.45) & cr \ + & pCx & "," & (pCy - tHh) & cr \ + & round(pCx - tHw * 0.62) & "," & round(pCy - tHh * 0.45) into tBase + break + default + -- platform: an axis-aligned rectangle + put (pCx - tHw) & "," & (pCy - tHh) & cr & (pCx + tHw) & "," & (pCy - tHh) & cr \ + & (pCx + tHw) & "," & (pCy + tHh) & cr & (pCx - tHw) & "," & (pCy + tHh) into tBase + end switch + put ((round(pAngleDeg) mod 360) + 360) mod 360 into tA + if tA is 0 then return tBase + put cos(tA * kPI / 180) into tC + put sin(tA * kPI / 180) into tS + put empty into tRot + repeat for each line tPt in tBase + put (item 1 of tPt) - pCx into tx + put (item 2 of tPt) - pCy into ty + put round(pCx + tx * tC - ty * tS) & "," & round(pCy + tx * tS + ty * tC) & cr after tRot + end repeat + delete the last char of tRot + return tRot +end terrainPoints + +-- Place a terrain piece: a static polygon body the user can size, rotate and +-- drag like any part. pW/pH default per kind; Load passes the saved size. +function placeTerrain pKind, pX, pY, pW, pH + local tName, tRef, tWH, tColor + set the itemDelimiter to comma + put terrainDefault(pKind) into tWH + if pW is empty or pW < 1 then put item 1 of tWH into pW + if pH is empty or pH < 1 then put item 2 of tWH into pH + put terrainColor(pKind) into tColor + put "cb_terrain_" & the milliseconds & "_" & random(99999) into tName + create graphic tName + put the long id of graphic tName into tRef + set the style of tRef to "polygon" + set the filled of tRef to true + set the backgroundColor of tRef to tColor + set the foregroundColor of tRef to "20,20,24" + set the lineSize of tRef to 1 + set the points of tRef to terrainPoints(pKind, round(pX), round(pY), pW, pH, 0) + set the loc of tRef to round(pX) & "," & round(pY) + b2kAddPolygon tRef, false -- static: never falls, never syncs + tagPart tRef, pKind, tColor, empty + set the uW of tRef to pW + set the uH of tRef to pH + set the uAngle of tRef to 0 + set the uFriction of tRef to 0.6 -- terrain grips by default + b2kSetFriction tRef, 0.6 + return tRef +end placeTerrain + +-- Rebuild a terrain piece's outline + static collision shape from its current +-- size and angle. Called on every resize/rotate; the body origin stays put. +on regenTerrain pCtrl + if pCtrl is empty then exit regenTerrain + local tCx, tCy + put item 1 of the loc of pCtrl into tCx + put item 2 of the loc of pCtrl into tCy + set the points of pCtrl to terrainPoints(the uKind of pCtrl, tCx, tCy, \ + numOr(the uW of pCtrl, 120), numOr(the uH of pCtrl, 26), numOr(the uAngle of pCtrl, 0)) + set the loc of pCtrl to tCx & "," & tCy -- keep centred on the body origin + b2kReshape pCtrl, "poly" + reapplyMaterial pCtrl +end regenTerrain + -- Tag a part with the data save/load needs, register it, apply the material. on tagPart pCtrl, pKind, pColor, pFile if pCtrl is empty then exit tagPart @@ -2444,6 +2576,8 @@ on buildSpan pKind, pA, pB, pEx, pEy put placeBox(round(tCx), round(tCy), max(8, round(tLen)), tHt, tColor) into tSeg set the uDensity of tSeg to tDens b2kSetDensity tSeg, tDens + set the uBounce of tSeg to 0 -- spans stay taut even when Bouncy mode is on + b2kSetBounce tSeg, 0 b2kMoveTo tSeg, round(tCx), round(tCy), tAng if tI is 1 then get connectJoint("pin", pA, tSeg, round(tJx0), round(tJy0), 0) else get connectJoint("pin", tPrev, tSeg, round(tJx0), round(tJy0), 0) @@ -2721,7 +2855,8 @@ on duplicatePart pCtrl put (item 2 of tLoc) + 26 into tNy if tNx > gArenaR then put (item 1 of tLoc) - 26 into tNx if tNy > gArenaB then put (item 2 of tLoc) - 26 into tNy - put rebuildPart(tKind, tNx, tNy, (the width of pCtrl), (the height of pCtrl), \ + put rebuildPart(tKind, tNx, tNy, numOr(the uW of pCtrl, the width of pCtrl), \ + numOr(the uH of pCtrl, the height of pCtrl), \ (the uColor of pCtrl), (the uFile of pCtrl)) into tNew if tNew is empty then exit duplicatePart if the uBounce of pCtrl is not empty then @@ -2906,6 +3041,11 @@ function partSpecial pCtrl local tS, tKind put the uKind of pCtrl into tKind put empty into tS + if kindIsTerrain(tKind) then -- static scenery: grip + baked-in angle + put addKV(tS, "fric=" & (the uFriction of pCtrl)) into tS + put addKV(tS, "tang=" & numOr(the uAngle of pCtrl, 0)) into tS + return tS + end if if kindHasBody(tKind) then put addKV(tS, "fric=" & (the uFriction of pCtrl)) into tS put addKV(tS, "dens=" & (the uDensity of pCtrl)) into tS @@ -3095,6 +3235,10 @@ function rebuildPart pKind, pX, pY, pW, pH, pColor, pFile return placePoly(pX, pY, round(pW / 2), pColor) case "anchor" return placeAnchor(pX, pY) + case "platform" + case "ramp" + case "hill" + return placeTerrain(pKind, pX, pY, pW, pH) case "image" return rebuildImage(pX, pY, pW, pH, pFile) case "balloon" @@ -3310,6 +3454,15 @@ on applyPartSpecial pCtrl, pSpecial if pCtrl is empty then exit applyPartSpecial local tKind, tV put the uKind of pCtrl into tKind + if kindIsTerrain(tKind) then -- restore grip + angle, then redraw the outline + put specialValue(pSpecial, "fric") into tV + if tV is not empty then set the uFriction of pCtrl to tV + put specialValue(pSpecial, "tang") into tV + if tV is empty then put 0 into tV + set the uAngle of pCtrl to tV + regenTerrain pCtrl + exit applyPartSpecial + end if if kindHasBody(tKind) then put specialValue(pSpecial, "fric") into tV if tV is not empty then @@ -3742,6 +3895,10 @@ function partProps pKind return "width,height,color,angle,friction" case "anchor" return "width,height,color" + case "platform" + case "ramp" + case "hill" + return "width,height,angle,friction,color" case "fan" return "fandir,fanforce,fansize,color" case "magnet" @@ -3766,6 +3923,7 @@ function propLabel pCtrl, pKey case "diameter" return "Diameter: " & numOr(the uW of pCtrl, the width of pCtrl) & " px" case "angle" + if kindIsTerrain(the uKind of pCtrl) then return "Angle: " & numOr(the uAngle of pCtrl, 0) & " deg" return "Angle: " & round(b2kAngle(pCtrl)) & " deg" case "color" return "Colour (cycle with +/−)" @@ -3874,10 +4032,18 @@ on adjustPartProp pCtrl, pKey, pDir else resizePart pCtrl, 0.87 break case "width" - setPartDimension pCtrl, "w", (the width of pCtrl) + pDir * kDimStep + if kindIsTerrain(the uKind of pCtrl) then + setPartDimension pCtrl, "w", numOr(the uW of pCtrl, 120) + pDir * kDimStep + else + setPartDimension pCtrl, "w", (the width of pCtrl) + pDir * kDimStep + end if break case "height" - setPartDimension pCtrl, "h", (the height of pCtrl) + pDir * kDimStep + if kindIsTerrain(the uKind of pCtrl) then + setPartDimension pCtrl, "h", numOr(the uH of pCtrl, 26) + pDir * kDimStep + else + setPartDimension pCtrl, "h", (the height of pCtrl) + pDir * kDimStep + end if break case "length" setPartDimension pCtrl, "w", (the width of pCtrl) + pDir * kDimStep @@ -3889,7 +4055,11 @@ on adjustPartProp pCtrl, pKey, pDir setPartDimension pCtrl, "d", (the width of pCtrl) + pDir * kDimStep break case "angle" - setPartAngle pCtrl, round(b2kAngle(pCtrl)) + pDir * 15 + if kindIsTerrain(the uKind of pCtrl) then + setPartAngle pCtrl, numOr(the uAngle of pCtrl, 0) + pDir * 15 + else + setPartAngle pCtrl, round(b2kAngle(pCtrl)) + pDir * 15 + end if break case "color" put partSwatches() into tPal @@ -4086,6 +4256,9 @@ function shapeOfKind pKind case "capsule" return "capsule" case "poly" + case "platform" + case "ramp" + case "hill" return "poly" end switch return "box" @@ -4225,6 +4398,15 @@ end clampDim on setPartDimension pCtrl, pAxis, pValue local tKind, tCx, tCy, tW, tH, tAng put the uKind of pCtrl into tKind + -- Terrain rebuilds its outline from uW/uH directly (its angle lives in the + -- points, not the body), so it sizes cleanly at any rotation. + if kindIsTerrain(tKind) then + put clampRange(pValue, kTerrainMin, kTerrainMax) into pValue + if pAxis is "h" then set the uH of pCtrl to pValue + else set the uW of pCtrl to pValue + regenTerrain pCtrl + exit setPartDimension + end if put item 1 of the loc of pCtrl into tCx put item 2 of the loc of pCtrl into tCy -- A rotated graphic's rect is its (inflated) bounding box, so resize in an @@ -4297,6 +4479,7 @@ function currentPropValue pCtrl, pKey case "thickness" return numOr(the uH of pCtrl, the height of pCtrl) case "angle" + if kindIsTerrain(the uKind of pCtrl) then return numOr(the uAngle of pCtrl, 0) return round(b2kAngle(pCtrl)) case "bounce" return numOr(the uBounce of pCtrl, 0) @@ -4893,6 +5076,12 @@ end jointSpringDampDefault -- Re-seat a part at an exact angle (degrees), keeping its position. Joints survive. on setPartAngle pCtrl, pDeg local tCx, tCy + -- Terrain bakes its angle into the points (a static body never re-syncs). + if kindIsTerrain(the uKind of pCtrl) then + set the uAngle of pCtrl to ((round(pDeg) mod 360) + 360) mod 360 + regenTerrain pCtrl + exit setPartAngle + end if if not kindHasBody(the uKind of pCtrl) then exit setPartAngle put item 1 of the loc of pCtrl into tCx put item 2 of the loc of pCtrl into tCy @@ -5105,7 +5294,8 @@ function niceName pId & "poly=Polygon" & cr & "image=Picture" & cr & "anchor=Anchor" & cr & "delete=Delete" & cr \ & "duplicate=Duplicate" & cr \ & "balloon=Helium Balloon" & cr & "bomb=Bomb" & cr & "plate=Pressure Plate" & cr \ - & "fan=Fan / Wind" & cr & "magnet=Magnet" & cr & "hinge=Hinge Joint" & cr \ + & "fan=Fan / Wind" & cr & "magnet=Magnet" & cr \ + & "platform=Platform" & cr & "ramp=Ramp" & cr & "hill=Hill" & cr & "hinge=Hinge Joint" & cr \ & "weld=Weld Joint" & cr & "rope=Rope Joint" & cr & "slider=Slider Joint" & cr \ & "wheel=Wheel Joint" & cr & "bridge=Bridge" & cr & "chain=Chain (rope)" into tNames local tLine @@ -5126,7 +5316,8 @@ function toolGlyph pId local tGlyphs put "drag=✥" & cr & "box=■" & cr & "ball=●" & cr & "capsule=▬" & cr & "poly=◆" & cr \ & "image=▦" & cr & "anchor=▼" & cr & "delete=✕" & cr & "duplicate=▣" & cr & "balloon=◯" & cr & "bomb=◉" & cr \ - & "plate=▭" & cr & "fan=▷" & cr & "magnet=◐" & cr & "hinge=○" & cr & "weld=▰" & cr \ + & "plate=▭" & cr & "fan=▷" & cr & "magnet=◐" & cr \ + & "platform=━" & cr & "ramp=◣" & cr & "hill=∩" & cr & "hinge=○" & cr & "weld=▰" & cr \ & "rope=∿" & cr & "slider=↔" & cr & "wheel=◎" & cr & "bridge=◠" & cr & "chain=∾" into tGlyphs local tLine set the itemDelimiter to "=" @@ -5171,6 +5362,12 @@ function toolHelp pId return "A wind zone that blows parts along." & cr & "A fan pushes any part inside its zone in the arrow's direction. Set the direction and strength in the inspector. No body — parts pass through it." case "magnet" return "A magnet that pulls parts toward it." & cr & "A magnet attracts nearby parts (or repels them). Set attract/repel, strength and reach in the inspector. No body — parts pass through it." + case "platform" + return "Place a solid ledge that never moves." & cr & "A platform is fixed scenery — a solid ledge that stays put. Rest parts on it, build floors and steps, or rotate it into an angled shelf. Set its size, angle and grip in the inspector." + case "ramp" + return "Place a fixed slope for rolling and sliding." & cr & "A ramp is a fixed wedge. Balls roll down it and parts slide along it. Rotate it for any incline and raise its grip to slow the slide — great for launches and runs." + case "hill" + return "Place a fixed rounded mound." & cr & "A hill is a fixed, rounded mound that parts roll over and settle against. Size and rotate it to shape your landscape." case "hinge" return "Pin two parts so they rotate." & cr & "A hinge is a pin two parts spin around — like an axle or a pendulum. Pin to an Anchor or empty space, switch Motor on, and it drives itself." case "weld" @@ -5281,7 +5478,7 @@ on buildRecipesMenu put the width of this card into tWide put the height of this card into tHigh put 280 into tW - put 448 into tH + put 482 into tH put (tWide - tW) div 2 into tL put (tHigh - tH) div 2 into tT makeBar "ui_recipescrim", 0, 0, tWide, tHigh, "10,11,15" @@ -5309,6 +5506,8 @@ on buildRecipesMenu makeRecipeBtn "cradle", "Newton's Cradle", tL + 16, tBy, tW - 32 add 34 to tBy makeRecipeBtn "dominoes", "Domino Run", tL + 16, tBy, tW - 32 + add 34 to tBy + makeRecipeBtn "terrainrun", "Terrain Run", tL + 16, tBy, tW - 32 add 40 to tBy makeRecipeBtn "stress", "Stress Test (+" & kStressBatch & " bodies)", tL + 16, tBy, tW - 32 add 40 to tBy @@ -5487,6 +5686,25 @@ on buildRecipeDominoes updateHud end buildRecipeDominoes +-- A ball rolls down a ramp, along a raised ledge and into a hill — the terrain set. +on buildRecipeTerrainRun + if gMode is "run" then toggleMode + clearAll + local tDeckTop, tBall + put gArenaB - 84 into tDeckTop -- shared surface for the pieces + get placeTerrain("platform", (gArenaL + gArenaR) div 2, tDeckTop + 14, 560, 28) + get placeTerrain("ramp", gArenaR - 206, tDeckTop - 65, 200, 130) -- on the deck's right end + get placeTerrain("hill", gArenaL + 252, tDeckTop - 40, 160, 80) -- a bump partway along + put placeBall(gArenaR - 142, tDeckTop - 150, 36, "232,182,92") into tBall + set the uBounce of tBall to 0 + b2kSetBounce tBall, 0 + set the uLaunchSpeed of tBall to 120 + set the uLaunchDir of tBall to 180 -- a nudge to start it down-slope + renderBuild + put "Terrain Run — press Run: the ball rolls down the ramp, along the ledge and into the hill. Resize, rotate or re-grip any terrain piece in the inspector." into gStatus + updateHud +end buildRecipeTerrainRun + -- Drop a batch of random bodies and run, to find where the frame rate gives out. on stressTest local tI, tX, tY