From 64d72f119bf0a802c1d6c52515fa13c5f2d36732 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 00:00:15 +0000 Subject: [PATCH 1/2] Add complete object & joint settings to the Contraption Builder Inspector: - Set width/height (box, image, plate, anchor), length/thickness (capsule) and diameter (ball, balloon, bomb) independently, replacing the single blind "Size" scale. Polygons keep uniform scaling. - Type an exact value by clicking any numeric setting; the +/- steppers still work. Both paths funnel through one setter so physics stays synced. - Expose linear/angular damping (air drag) and the bullet/CCD flag. - Organise settings into Shape / Physics / Special tabs so every kind fits without overflowing the 8-row panel. Per-joint inspector (new): - Select a joint with the Drag tool to edit its motor (on/off, speed), limits (on/off, low/high) and spring (hertz, damping), plus rope min/max length. Hinge/weld/rope/slider/wheel each show the relevant set. Persistence: - Save/load now round-trips part damping, bullet and density and the new joint settings. All fields are append-only, so older CB1/CB2 saves still load and fall back to defaults. Kit: - Add b2kMotorOff / b2kHingeLimitOff / b2kSliderMotorOff / b2kSliderLimitOff / b2kWheelMotorOff (existing wrappers only enabled features) in both the embedded and source Kit, documented in kit-reference.md. No existing behaviour removed. https://claude.ai/code/session_01AyoQTVuyLtPMrR5oVW6FFr --- docs/kit-reference.md | 5 + ...box2dxt-contraption-builder.livecodescript | 928 +++++++++++++++++- src/box2dxt-kit.livecodescript | 22 + 3 files changed, 933 insertions(+), 22 deletions(-) diff --git a/docs/kit-reference.md b/docs/kit-reference.md index 378017e..b38bd74 100644 --- a/docs/kit-reference.md +++ b/docs/kit-reference.md @@ -188,6 +188,11 @@ helpers or to `b2kRemoveJoint`. | `b2kRopeLength(joint)` → pixels | Current distance-joint length. | | `b2kSpring joint, hertz [,damping]` | Make a rope (distance) joint springy. | | `b2kWeldSpring joint, hertz [,damping]` | Make a weld springy (0 hertz = rigid). | +| `b2kMotorOff joint` | Turn a hinge motor off (free swing). | +| `b2kHingeLimitOff joint` | Remove a hinge's angle limits. | +| `b2kSliderMotorOff joint` | Turn a slider motor off. | +| `b2kSliderLimitOff joint` | Remove a slider's travel limits. | +| `b2kWheelMotorOff joint` | Turn a wheel motor off. | ## Drag & events diff --git a/examples/box2dxt-contraption-builder.livecodescript b/examples/box2dxt-contraption-builder.livecodescript index 2cc70dc..358620a 100644 --- a/examples/box2dxt-contraption-builder.livecodescript +++ b/examples/box2dxt-contraption-builder.livecodescript @@ -1003,6 +1003,28 @@ command b2kWheelSpring pJoint, pHertz, pDamping b2WheelEnableSpring pJoint, true, pHertz, pDamping end b2kWheelSpring +-- OFF switches: the b2k… joint wrappers above only ever turn a feature ON, so +-- these let callers disable a motor or limit again (free swing / unbounded slide). +command b2kMotorOff pJoint + if pJoint is not empty and pJoint > 0 then b2RevoluteEnableMotor pJoint, false, 0, 0 +end b2kMotorOff + +command b2kHingeLimitOff pJoint + if pJoint is not empty and pJoint > 0 then b2RevoluteEnableLimit pJoint, false, 0, 0 +end b2kHingeLimitOff + +command b2kSliderMotorOff pJoint + if pJoint is not empty and pJoint > 0 then b2PrismaticEnableMotor pJoint, false, 0, 0 +end b2kSliderMotorOff + +command b2kSliderLimitOff pJoint + if pJoint is not empty and pJoint > 0 then b2PrismaticEnableLimit pJoint, false, 0, 0 +end b2kSliderLimitOff + +command b2kWheelMotorOff pJoint + if pJoint is not empty and pJoint > 0 then b2WheelEnableMotor pJoint, false, 0, 0 +end b2kWheelMotorOff + command b2kRemoveJoint pJoint if pJoint is not empty and pJoint > 0 then b2DestroyJoint pJoint end b2kRemoveJoint @@ -1243,6 +1265,8 @@ local gRingN, gRingX, gRingY, gRingF local gDynParts, gDynDirty -- selection / inspector / onboarding / stress-test bookkeeping local gSelPart, gHoverId, gOnbStep, gStressCount +local gSelJoint -- selected joint index (1..gJN), empty if none +local gInspTab -- active inspector tab: shape | physics | special local gPreRun -- snapshot of the build, taken when Run starts constant kShapeTools = "drag,box,ball,capsule,poly,image,anchor,delete,duplicate" @@ -1278,6 +1302,10 @@ constant kFanW = 130, kFanH = 150, kFanForce = 900 constant kMagnetRange = 220, kMagnetForce = 1400, kMagnetMinDist = 16 constant kStressBatch = 50 constant kSizeMin = 16, kSizeMax = 240 +-- 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) +constant kEntryKeys = "width,height,length,thickness,diameter,size,bounce,density,friction,gravity,lindamp,angdamp,blastradius,blastpower,fanforce,fansize,magforce,magrange" -- ===================================================================== -- Lifecycle @@ -1312,6 +1340,8 @@ on startCB put empty into gFps put the milliseconds into gFpsTime put empty into gSelPart + put empty into gSelJoint + put "shape" into gInspTab put empty into gHoverId put 0 into gStressCount resetSceneState @@ -1468,17 +1498,21 @@ on makeInspector put kTopBarH + kAccentH into tTop makeBar "ui_inspbg", tL, tTop, tWide, tHigh - kStatusH, "26,28,35" makeLabel "ui_insp_title", tL + 12, tTop + 10, tWide - 12, tTop + 34, "Inspector", 14, "236,238,245", true - makeField "ui_insp_body", tL + 12, tTop + 40, tWide - 12, tTop + 150, 11, "190,194,206" + makeField "ui_insp_body", tL + 12, tTop + 40, tWide - 12, tTop + 118, 11, "190,194,206" + -- settings tabs: group a part's many settings so each pane stays short + makeAction "tab_shape", "Shape", tL + 12, tTop + 124, 70, 24 + makeAction "tab_physics", "Physics", tL + 86, tTop + 124, 76, 24 + makeAction "tab_special", "Special", tL + 166, tTop + 124, 70, 24 -- a clearly-marked, divided settings block so adjustable values stand out makeBar "ui_settings_div", tL + 12, tTop + 156, tWide - 12, tTop + 157, "60,66,80" makeLabel "ui_settings_hdr", tL + 12, tTop + 162, tWide - 12, tTop + 182, "PART SETTINGS", 11, "120,220,150", true makeField "ui_settings_hint", tL + 12, tTop + 186, tWide - 12, tTop + 250, 11, "150,154,166" put tTop + 186 into tRy - repeat with tI = 1 to 8 - makeField ("ui_prop_lbl_" & tI), tL + 12, tRy + 3, tWide - 92, tRy + 27, 11, "232,236,246" + repeat with tI = 1 to kPropRows + makeField ("ui_prop_lbl_" & tI), tL + 12, tRy + 3, tWide - 92, tRy + 25, 11, "232,236,246" makePropBtn ("prop_dec_" & tI), "−", tWide - 84, tRy, 32, 26 makePropBtn ("prop_inc_" & tI), "+", tWide - 44, tRy, 32, 26 - add 30 to tRy + add 28 to tRy end repeat add 6 to tRy makeBar "ui_options_div", tL + 12, tRy, tWide - 12, tRy + 1, "60,66,80" @@ -1691,6 +1725,15 @@ on mouseDown jointClick tx, ty exit mouseDown end if + -- Drag tool: a click on a joint marker selects that joint for editing. + if gTool is "drag" then + local tJHit + put jointUnderClick(tx, ty) into tJHit + if tJHit is not empty then + selectJoint tJHit + exit mouseDown + end if + end if put partUnderClick(tx, ty) into tHit if gTool is "delete" then if tHit is not empty then @@ -1765,6 +1808,11 @@ on handleUiClick hideRecipesMenu exit handleUiClick end if + -- Click a settings value to type an exact number (steppers stay on the +/- keys). + if "ui_prop_lbl_" is in tName then + editPropValue (the uPropKey of the target) + exit handleUiClick + end if -- Picking a build tool while the simulation runs drops you back into Build. if ("ui_tool_" is in tName or "ui_joint_" is in tName) and gMode is "run" then toggleMode if "ui_tool_" is in tName then @@ -1772,12 +1820,14 @@ on handleUiClick put empty into gJointTool resetPend deselectPart + deselectJoint put "Tool: " & friendlyKind(gTool) into gStatus showInspectorForTool else if "ui_joint_" is in tName then put the uJointId of the target into gJointTool resetPend deselectPart + deselectJoint put "Joint: " & gJointTool & " — click part A, then part B." into gStatus showInspectorForTool else if "ui_act_" is in tName then @@ -1793,6 +1843,10 @@ on handleAction pId adjustProp pId exit handleAction end if + if char 1 to 4 of pId is "tab_" then + selectInspTab (char 5 to -1 of pId) + exit handleAction + end if switch pId case "mode" toggleMode @@ -1881,6 +1935,7 @@ on toggleMode put "run" into gMode put empty into gJointTool resetPend + deselectJoint put true into gMotorsRunning armFuses b2kStart -- fresh loop: gravity, motors, mouse drag @@ -2328,6 +2383,8 @@ function connectJoint pKind, pA, pB, pX, pY, pMotorSpeed b2kWheelMotor tJ, pMotorSpeed, kWheelTorque end if put pMotorSpeed into gJMotor[gJN] + set the uMotorOn of graphic ("cb_joint_" & gJN) to true + set the uMotorSpeed of graphic ("cb_joint_" & gJN) to pMotorSpeed end if return true end connectJoint @@ -2375,6 +2432,17 @@ on recordJoint pKind, pJoint, pA, pB, pX, pY, pVis set the foregroundColor of graphic tG to "200,206,220" end if end if + -- default editable joint settings, stored on the marker (like parts' u-props) + set the uMotorOn of graphic tG to false + set the uMotorSpeed of graphic tG to gMotorSpeed + set the uLimitOn of graphic tG to false + set the uLimitLo of graphic tG to jointLimitLoDefault(pKind) + set the uLimitHi of graphic tG to jointLimitHiDefault(pKind) + set the uSpringHz of graphic tG to jointSpringHzDefault(pKind) + set the uSpringDamp of graphic tG to jointSpringDampDefault(pKind) + set the uMinLen of graphic tG to 0 + set the uMaxLen of graphic tG to 200 + set the uRangeSet of graphic tG to false end recordJoint -- Redraw every joint marker. dot = pivot bead on B; rod = arm from a fixed pivot @@ -2515,6 +2583,7 @@ on duplicatePart pCtrl end duplicatePart on removeJointAt pIndex + if gSelJoint is pIndex then deselectJoint try b2kRemoveJoint gJHandle[pIndex] end try @@ -2564,6 +2633,7 @@ on clearJoints end clearJoints on resetJointArrays + put empty into gSelJoint put 0 into gJN put empty into gJKind put empty into gJHandle @@ -2682,9 +2752,13 @@ function partSpecial pCtrl put empty into tS if kindHasBody(tKind) then put addKV(tS, "fric=" & (the uFriction of pCtrl)) into tS + put addKV(tS, "dens=" & (the uDensity of pCtrl)) into tS put addKV(tS, "grav=" & (the uGravity of pCtrl)) into tS put addKV(tS, "frot=" & (the uFixedRot of pCtrl is true)) into tS put addKV(tS, "btype=" & (the uBodyType of pCtrl)) into tS + put addKV(tS, "ldmp=" & (the uLinDamp of pCtrl)) into tS + put addKV(tS, "admp=" & (the uAngDamp of pCtrl)) into tS + put addKV(tS, "bul=" & (the uBullet of pCtrl is true)) into tS end if switch tKind case "fan" @@ -2739,7 +2813,8 @@ function jointLine pIndex if gJCb[pIndex] is not empty then put the uPartId of gJCb[pIndex] into tIdB end try return "joint" & tab & gJKind[pIndex] & tab & tIdA & tab & tIdB & tab & \ - gJPx[pIndex] & tab & gJPy[pIndex] & tab & gJMotor[pIndex] + gJPx[pIndex] & tab & gJPy[pIndex] & tab & gJMotor[pIndex] & tab & \ + jointSpecial(pIndex) end jointLine on loadContraption @@ -2773,7 +2848,7 @@ end loadContraption on rebuildFromText pData local tLine, tRec, tMap, tNew local tId, tKind, tX, tY, tW, tH, tBounce, tColor, tFileRef, tSpecial - local tIdA, tIdB, tPx, tPy, tMotor, tA, tB + local tIdA, tIdB, tPx, tPy, tMotor, tA, tB, tJSpecial clearAll put empty into tMap lock screen @@ -2808,12 +2883,13 @@ on rebuildFromText pData put item 5 of tLine into tPx put item 6 of tLine into tPy put item 7 of tLine into tMotor + put item 8 of tLine into tJSpecial set the itemDelimiter to comma put tMap[tIdA] into tA if tIdB is 0 then put empty into tB else put tMap[tIdB] into tB if tA is not empty and (tB is not empty or tKind is "hinge") then - get connectJoint(tKind, tA, tB, tPx, tPy, tMotor) + if connectJoint(tKind, tA, tB, tPx, tPy, tMotor) then applyJointSpecial gJN, tJSpecial end if else set the itemDelimiter to comma @@ -3074,6 +3150,11 @@ on applyPartSpecial pCtrl, pSpecial set the uFriction of pCtrl to tV b2kSetFriction pCtrl, tV end if + put specialValue(pSpecial, "dens") into tV + if tV is not empty then + set the uDensity of pCtrl to tV + b2kSetDensity pCtrl, tV + end if put specialValue(pSpecial, "grav") into tV if tV is not empty then set the uGravity of pCtrl to tV @@ -3087,6 +3168,20 @@ on applyPartSpecial pCtrl, pSpecial set the uBodyType of pCtrl to "static" b2kSetType pCtrl, "static" end if + put specialValue(pSpecial, "ldmp") into tV + if tV is not empty then + set the uLinDamp of pCtrl to tV + b2kSetDamping pCtrl, tV, empty + end if + put specialValue(pSpecial, "admp") into tV + if tV is not empty then + set the uAngDamp of pCtrl to tV + b2kSetDamping pCtrl, empty, tV + end if + if specialValue(pSpecial, "bul") is "true" then + set the uBullet of pCtrl to true + b2kSetBullet pCtrl, true + end if end if switch tKind case "fan" @@ -3378,6 +3473,7 @@ end deleteByPrefix on selectPart pCtrl if pCtrl is empty then exit selectPart deselectPart + deselectJoint put pCtrl into gSelPart try set the uSelFg of pCtrl to the foregroundColor of pCtrl @@ -3413,7 +3509,8 @@ on hoverHelp pId end hoverHelp on restoreInspector - if gSelPart is not empty then showPartInspector gSelPart + if gSelJoint is not empty then showJointInspector gSelJoint + else if gSelPart is not empty then showPartInspector gSelPart else showInspectorForTool end restoreInspector @@ -3425,39 +3522,50 @@ end setInspector -- Show a selected part's name, what it does, and the values you can change. on showPartInspector pCtrl if pCtrl is empty then exit showPartInspector - local tKind, tKey, tRow + local tKind, tKey, tRow, tAvail put the uKind of pCtrl into tKind setInspector ("Selected: " & niceName(tKind)), (line 2 of toolHelp(tKind)) clearPropRows - put 0 into tRow + put partTabs(tKind) into tAvail + if tAvail is empty then + setSettingsHint "This part has no adjustable settings." + exit showPartInspector + end if set the itemDelimiter to comma + if gInspTab is not among the items of tAvail then put item 1 of tAvail into gInspTab + refreshInspTabs tAvail + put 0 into tRow repeat for each item tKey in partProps(tKind) + if propGroup(tKey) is not gInspTab then next repeat add 1 to tRow - if tRow > 8 then exit repeat + if tRow > kPropRows then exit repeat showPropRow tRow, propLabel(pCtrl, tKey), tKey end repeat if tRow > 0 then setSettingsHint empty -- rows are showing; hide the prompt - else setSettingsHint "This part has no adjustable settings." + else setSettingsHint "Nothing to adjust on this tab." end showPartInspector -- The ordered list of adjustable settings shown for each kind of part. function partProps pKind switch pKind case "box" + return "width,height,color,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet" + case "image" + return "width,height,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet" case "ball" + return "diameter,color,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet" case "capsule" + return "length,thickness,color,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet" case "poly" - return "size,color,bounce,density,friction,gravity,fixedrot,bodytype" - case "image" - return "size,bounce,density,friction,gravity,fixedrot,bodytype" + return "size,color,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet" case "balloon" - return "size,color,bounce,density,friction,gravity" + return "diameter,color,bounce,density,friction,gravity" case "bomb" - return "size,color,bounce,density,blastradius,blastpower,bombfuse" + return "diameter,color,bounce,density,blastradius,blastpower,bombfuse" case "plate" - return "size,color,friction" + return "width,height,color,friction" case "anchor" - return "size,color" + return "width,height,color" case "fan" return "fandir,fanforce,fansize,color" case "magnet" @@ -3471,6 +3579,16 @@ function propLabel pCtrl, pKey switch pKey case "size" return "Size: " & (the width of pCtrl) & " px" + case "width" + return "Width: " & (the width of pCtrl) & " px" + case "height" + return "Height: " & (the height of pCtrl) & " px" + case "length" + return "Length: " & (the width of pCtrl) & " px" + case "thickness" + return "Thickness: " & (the height of pCtrl) & " px" + case "diameter" + return "Diameter: " & (the width of pCtrl) & " px" case "color" return "Colour (cycle with +/−)" case "bounce" @@ -3487,6 +3605,12 @@ function propLabel pCtrl, pKey case "bodytype" if the uBodyType of pCtrl is "static" then return "Movement: fixed in place" return "Movement: free-moving" + case "lindamp" + return "Air drag (move): " & numOr(the uLinDamp of pCtrl, 0) + case "angdamp" + return "Air drag (spin): " & numOr(the uAngDamp of pCtrl, 0) + case "bullet" + return "Fast-collision: " & yesNo(the uBullet of pCtrl is true) case "blastradius" return "Blast size: " & (the uBlastRadius of pCtrl) case "blastpower" @@ -3511,9 +3635,10 @@ end propLabel on clearPropRows local tI - repeat with tI = 1 to 8 + repeat with tI = 1 to kPropRows hidePropRow tI end repeat + hideInspTabs setSettingsHint "Select a part on the stage to adjust its settings here." end clearPropRows @@ -3526,6 +3651,7 @@ end hidePropRow on showPropRow pRow, pLabel, pKey if there is a field ("ui_prop_lbl_" & pRow) then put pLabel into field ("ui_prop_lbl_" & pRow) + set the uPropKey of field ("ui_prop_lbl_" & pRow) to pKey set the visible of field ("ui_prop_lbl_" & pRow) to true end if if there is a button ("ui_act_prop_dec_" & pRow) then @@ -3540,22 +3666,42 @@ end showPropRow -- A +/- click on an inspector property row. on adjustProp pId - if gSelPart is empty then exit adjustProp local tDir if char 6 to 8 of pId is "inc" then put 1 into tDir else put -1 into tDir + if gSelJoint is not empty then + adjustJointProp gSelJoint, (the uPropKey of the target), tDir + showJointInspector gSelJoint + exit adjustProp + end if + if gSelPart is empty then exit adjustProp adjustPartProp gSelPart, (the uPropKey of the target), tDir showPartInspector gSelPart if gMode is "build" then renderBuild end adjustProp on adjustPartProp pCtrl, pKey, pDir - local tB, tD, tF, tNewDir, tMf, tMr, tFr, tG, tBr, tPow, tFs, tIdx, tPal, tCnt + local tB, tD, tF, tNewDir, tMf, tMr, tFr, tG, tBr, tPow, tFs, tIdx, tPal, tCnt, tLd, tAd switch pKey case "size" if pDir > 0 then resizePart pCtrl, 1.15 else resizePart pCtrl, 0.87 break + case "width" + setPartDimension pCtrl, "w", (the width of pCtrl) + pDir * kDimStep + break + case "height" + setPartDimension pCtrl, "h", (the height of pCtrl) + pDir * kDimStep + break + case "length" + setPartDimension pCtrl, "w", (the width of pCtrl) + pDir * kDimStep + break + case "thickness" + setPartDimension pCtrl, "h", (the height of pCtrl) + pDir * kDimStep + break + case "diameter" + setPartDimension pCtrl, "d", (the width of pCtrl) + pDir * kDimStep + break case "color" put partSwatches() into tPal put the number of lines of tPal into tCnt @@ -3608,6 +3754,28 @@ on adjustPartProp pCtrl, pKey, pDir else set the uBodyType of pCtrl to "static" b2kSetType pCtrl, (the uBodyType of pCtrl) break + case "lindamp" + put the uLinDamp of pCtrl into tLd + if tLd is empty then put 0 into tLd + put tLd + pDir * 0.25 into tLd + if tLd < 0 then put 0 into tLd + if tLd > 5 then put 5 into tLd + set the uLinDamp of pCtrl to tLd + b2kSetDamping pCtrl, tLd, empty + break + case "angdamp" + put the uAngDamp of pCtrl into tAd + if tAd is empty then put 0 into tAd + put tAd + pDir * 0.25 into tAd + if tAd < 0 then put 0 into tAd + if tAd > 5 then put 5 into tAd + set the uAngDamp of pCtrl to tAd + b2kSetDamping pCtrl, empty, tAd + break + case "bullet" + set the uBullet of pCtrl to not (the uBullet of pCtrl is true) + b2kSetBullet pCtrl, (the uBullet of pCtrl is true) + break case "blastradius" put the uBlastRadius of pCtrl into tBr if tBr is empty then put kBombRadius into tBr @@ -3742,6 +3910,722 @@ function swatchIndexOf pCtrl, pPal return 0 end swatchIndexOf +-- ===================================================================== +-- Inspector tabs, exact-value entry, per-axis sizing, and joint settings +-- ===================================================================== + +-- Which settings tab a property key belongs to. +function propGroup pKey + if pKey is among the items of "width,height,length,thickness,diameter,size,color" then return "shape" + if pKey is among the items of "bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet" then return "physics" + return "special" +end propGroup + +-- The tabs that actually have settings for this kind (so empty tabs stay hidden). +function partTabs pKind + local tKey, tHasShape, tHasPhys, tHasSpec, tOut + put false into tHasShape + put false into tHasPhys + put false into tHasSpec + set the itemDelimiter to comma + repeat for each item tKey in partProps(pKind) + switch propGroup(tKey) + case "shape" + put true into tHasShape + break + case "physics" + put true into tHasPhys + break + case "special" + put true into tHasSpec + break + end switch + end repeat + put empty into tOut + if tHasShape then put "shape" into tOut + if tHasPhys then put addItem(tOut, "physics") into tOut + if tHasSpec then put addItem(tOut, "special") into tOut + return tOut +end partTabs + +-- Append an item to a comma list (small sibling of addKV for ; lists). +function addItem pList, pItem + if pList is empty then return pItem + return pList & "," & pItem +end addItem + +on selectInspTab pTab + put pTab into gInspTab + if gSelPart is not empty then showPartInspector gSelPart +end selectInspTab + +-- Show only the tabs a part offers, highlighting the active one. +on refreshInspTabs pAvail + local tT + set the itemDelimiter to comma + repeat for each item tT in "shape,physics,special" + if there is not a button ("ui_act_tab_" & tT) then next repeat + if tT is among the items of pAvail then + set the visible of button ("ui_act_tab_" & tT) to true + if tT is gInspTab then + set the backgroundColor of button ("ui_act_tab_" & tT) to "72,190,130" + set the textColor of button ("ui_act_tab_" & tT) to "255,255,255" + else + set the backgroundColor of button ("ui_act_tab_" & tT) to "44,48,58" + set the textColor of button ("ui_act_tab_" & tT) to "196,200,212" + end if + else + set the visible of button ("ui_act_tab_" & tT) to false + end if + end repeat +end refreshInspTabs + +on hideInspTabs + local tT + set the itemDelimiter to comma + repeat for each item tT in "shape,physics,special" + if there is a button ("ui_act_tab_" & tT) then set the visible of button ("ui_act_tab_" & tT) to false + end repeat +end hideInspTabs + +-- A custom-property value, or a fallback when it has never been set. +function numOr pV, pDefault + if pV is empty then return pDefault + return pV +end numOr + +function clampRange pV, pLo, pHi + if pV < pLo then return pLo + if pV > pHi then return pHi + return pV +end clampRange + +-- Keep a part dimension within the allowed pixel range. +function clampDim pV + if pV is empty or pV is not a number then return kSizeMin + put round(pV) into pV + if pV < kSizeMin then return kSizeMin + if pV > kSizeMax then return kSizeMax + return pV +end clampDim + +-- Set ONE axis of a part to an exact pixel size, then re-fit its physics shape on +-- the same body so joints survive. pAxis is "w", "h", or "d" (both, for balls). +on setPartDimension pCtrl, pAxis, pValue + local tKind, tCx, tCy, tW, tH + put the uKind of pCtrl into tKind + put item 1 of the loc of pCtrl into tCx + put item 2 of the loc of pCtrl into tCy + put the width of pCtrl into tW + put the height of pCtrl into tH + put clampDim(pValue) into pValue + if pAxis is "w" then put pValue into tW + else if pAxis is "h" then put pValue into tH + else if pAxis is "d" then + put pValue into tW + put pValue into tH + end if + set the width of pCtrl to tW + set the height of pCtrl to tH + set the loc of pCtrl to tCx & "," & tCy + if kindHasBody(tKind) then + b2kReshape pCtrl, shapeOfKind(tKind) + reapplyMaterial pCtrl + end if +end setPartDimension + +-- Click a settings value to type an exact number (parts or joints). +on editPropValue pKey + if pKey is empty then exit editPropValue + if gSelJoint is not empty then + editJointValue pKey + exit editPropValue + end if + if gSelPart is empty then exit editPropValue + set the itemDelimiter to comma + if pKey is not among the items of kEntryKeys then exit editPropValue + ask ("Set " & propEditName(gSelPart, pKey) & ":") with currentPropValue(gSelPart, pKey) + if it is empty or the result is "Cancel" then exit editPropValue + if it is not a number then + put "Please type a number." into gStatus + updateHud + exit editPropValue + end if + applyPropValue gSelPart, pKey, it + showPartInspector gSelPart + if gMode is "build" then renderBuild +end editPropValue + +-- A readable name for the entry dialog, taken from the row label (before the colon). +function propEditName pCtrl, pKey + local tL + put propLabel(pCtrl, pKey) into tL + if ":" is in tL then return char 1 to (offset(":", tL) - 1) of tL + return pKey +end propEditName + +function currentPropValue pCtrl, pKey + switch pKey + case "width" + case "length" + case "diameter" + case "size" + return the width of pCtrl + case "height" + case "thickness" + return the height of pCtrl + case "bounce" + return numOr(the uBounce of pCtrl, 0) + case "density" + return numOr(the uDensity of pCtrl, 1) + case "friction" + return numOr(the uFriction of pCtrl, 0.4) + case "gravity" + return numOr(the uGravity of pCtrl, 1) + case "lindamp" + return numOr(the uLinDamp of pCtrl, 0) + case "angdamp" + return numOr(the uAngDamp of pCtrl, 0) + case "blastradius" + return numOr(the uBlastRadius of pCtrl, kBombRadius) + case "blastpower" + return numOr(the uBlastPower of pCtrl, kBombPower) + case "fanforce" + return numOr(the uFanForce of pCtrl, kFanForce) + case "fansize" + return numOr(the uFanScale of pCtrl, 1) + case "magforce" + return numOr(the uMagForce of pCtrl, kMagnetForce) + case "magrange" + return numOr(the uMagRange of pCtrl, kMagnetRange) + end switch + return empty +end currentPropValue + +-- Apply an exact typed value to a part (clamped), syncing physics like the steppers. +on applyPropValue pCtrl, pKey, pVal + switch pKey + case "width" + case "length" + setPartDimension pCtrl, "w", pVal + break + case "height" + case "thickness" + setPartDimension pCtrl, "h", pVal + break + case "diameter" + setPartDimension pCtrl, "d", pVal + break + case "size" + if (the width of pCtrl) >= 1 then resizePart pCtrl, (clampDim(pVal) / (the width of pCtrl)) + break + case "bounce" + put clampRange(pVal, 0, 0.9) into pVal + set the uBounce of pCtrl to pVal + b2kSetBounce pCtrl, pVal + break + case "density" + put clampRange(pVal, 0.3, 3) into pVal + set the uDensity of pCtrl to pVal + b2kSetDensity pCtrl, pVal + break + case "friction" + put clampRange(pVal, 0, 2) into pVal + set the uFriction of pCtrl to pVal + b2kSetFriction pCtrl, pVal + break + case "gravity" + put clampRange(pVal, -2, 3) into pVal + set the uGravity of pCtrl to pVal + b2kSetGravityScale pCtrl, pVal + break + case "lindamp" + put clampRange(pVal, 0, 5) into pVal + set the uLinDamp of pCtrl to pVal + b2kSetDamping pCtrl, pVal, empty + break + case "angdamp" + put clampRange(pVal, 0, 5) into pVal + set the uAngDamp of pCtrl to pVal + b2kSetDamping pCtrl, empty, pVal + break + case "blastradius" + set the uBlastRadius of pCtrl to clampRange(pVal, 60, 500) + break + case "blastpower" + set the uBlastPower of pCtrl to clampRange(pVal, 200, 2000) + break + case "fanforce" + set the uFanForce of pCtrl to clampRange(pVal, 0, 3000) + break + case "fansize" + set the uFanScale of pCtrl to clampRange(pVal, 0.5, 2.5) + drawFanShape pCtrl + break + case "magforce" + set the uMagForce of pCtrl to clampRange(pVal, 100, 4000) + break + case "magrange" + set the uMagRange of pCtrl to clampRange(pVal, 60, 520) + break + end switch +end applyPropValue + +-- ---- Per-joint inspector -------------------------------------------- + +-- The on-screen point that selects a joint (its marker bead / link midpoint). +function jointHandleLoc pIndex + local tVis, tG, tA, tB + put gJVis[pIndex] into tVis + put "cb_joint_" & pIndex into tG + if there is not a graphic tG then return empty + if tVis is "dot" then return the loc of graphic tG + if tVis is "rod" then + put line 1 of the points of graphic tG into tA + put line 2 of the points of graphic tG into tB + if tA is empty or tB is empty then return the loc of graphic tG + return midOf(tA, tB) + end if + if gJCa[pIndex] is empty then return empty + put the loc of gJCa[pIndex] into tA + if gJCb[pIndex] is empty then put tA into tB + else put the loc of gJCb[pIndex] into tB + return midOf(tA, tB) +end jointHandleLoc + +function midOf pA, pB + return round((item 1 of pA + item 1 of pB) / 2) & "," & round((item 2 of pA + item 2 of pB) / 2) +end midOf + +-- The live joint whose handle is nearest the click (within kJointHitR), or empty. +function jointUnderClick pX, pY + local tI, tBest, tBestD, tH, tD + put empty into tBest + put empty into tBestD + if gJN < 1 then return empty + repeat with tI = 1 to gJN + if gJKind[tI] is empty then next repeat + put jointHandleLoc(tI) into tH + if tH is empty then next repeat + put sqrt((pX - item 1 of tH)^2 + (pY - item 2 of tH)^2) into tD + if tD <= kJointHitR and (tBest is empty or tD < tBestD) then + put tI into tBest + put tD into tBestD + end if + end repeat + return tBest +end jointUnderClick + +on selectJoint pIndex + if pIndex is empty then exit selectJoint + if gJKind[pIndex] is empty then exit selectJoint + deselectPart + deselectJoint + put pIndex into gSelJoint + local tG + put "cb_joint_" & pIndex into tG + if there is a graphic tG then + try + set the uSelLine of graphic tG to the lineSize of graphic tG + set the uSelFg of graphic tG to the foregroundColor of graphic tG + set the lineSize of graphic tG to (the lineSize of graphic tG) + 3 + set the foregroundColor of graphic tG to "255,255,255" + end try + end if + showJointInspector pIndex +end selectJoint + +on deselectJoint + if gSelJoint is empty then exit deselectJoint + local tG + put "cb_joint_" & gSelJoint into tG + if there is a graphic tG then + try + set the lineSize of graphic tG to the uSelLine of graphic tG + set the foregroundColor of graphic tG to the uSelFg of graphic tG + end try + end if + put empty into gSelJoint + clearPropRows +end deselectJoint + +-- Show a selected joint's name, what it does, and the values you can change. +on showJointInspector pIndex + if pIndex is empty then exit showJointInspector + if gJKind[pIndex] is empty then exit showJointInspector + local tKind, tKey, tRow, tMk + put gJKind[pIndex] into tKind + put the long id of graphic ("cb_joint_" & pIndex) into tMk + setInspector ("Selected: " & niceName(tKind)), (line 2 of toolHelp(tKind)) + clearPropRows + hideInspTabs + put 0 into tRow + set the itemDelimiter to comma + repeat for each item tKey in jointProps(tKind) + add 1 to tRow + if tRow > kPropRows then exit repeat + showPropRow tRow, jointPropLabel(tMk, tKind, tKey), tKey + end repeat + if tRow > 0 then setSettingsHint empty + else setSettingsHint "This joint has no adjustable settings." +end showJointInspector + +-- The ordered list of adjustable settings shown for each kind of joint. +function jointProps pKind + switch pKind + case "hinge" + return "jmotoron,jmotorspeed,jlimiton,jlimitlo,jlimithi" + case "slider" + return "jmotoron,jmotorspeed,jlimiton,jlimitlo,jlimithi" + case "wheel" + return "jmotoron,jmotorspeed,jspringhz,jspringdamp" + case "rope" + return "jminlen,jmaxlen,jspringhz,jspringdamp" + case "weld" + return "jspringhz,jspringdamp" + end switch + return empty +end jointProps + +-- The human-readable "Name: value" shown on a joint property row. +function jointPropLabel pMk, pKind, pKey + local tUnit + if pKind is "slider" then put " px/s" into tUnit + else put " deg/s" into tUnit + switch pKey + case "jmotoron" + return "Motor: " & onOff(the uMotorOn of pMk is true) + case "jmotorspeed" + return "Motor speed: " & numOr(the uMotorSpeed of pMk, gMotorSpeed) & tUnit + case "jlimiton" + return "Limits: " & onOff(the uLimitOn of pMk is true) + case "jlimitlo" + if pKind is "slider" then return "Limit min: " & numOr(the uLimitLo of pMk, -100) & " px" + return "Limit min: " & numOr(the uLimitLo of pMk, -45) & " deg" + case "jlimithi" + if pKind is "slider" then return "Limit max: " & numOr(the uLimitHi of pMk, 100) & " px" + return "Limit max: " & numOr(the uLimitHi of pMk, 45) & " deg" + case "jspringhz" + return "Springiness: " & numOr(the uSpringHz of pMk, 0) & " Hz" + case "jspringdamp" + return "Spring damping: " & numOr(the uSpringDamp of pMk, 0.5) + case "jminlen" + return "Min length: " & numOr(the uMinLen of pMk, 0) & " px" + case "jmaxlen" + return "Max length: " & numOr(the uMaxLen of pMk, 200) & " px" + end switch + return pKey +end jointPropLabel + +function onOff pFlag + if pFlag is true then return "on" + return "off" +end onOff + +on adjustJointProp pIndex, pKey, pDir + local tMk, tV + put the long id of graphic ("cb_joint_" & pIndex) into tMk + switch pKey + case "jmotoron" + set the uMotorOn of tMk to not (the uMotorOn of tMk is true) + applyJointMotor pIndex + break + case "jmotorspeed" + put numOr(the uMotorSpeed of tMk, gMotorSpeed) + pDir * 30 into tV + set the uMotorSpeed of tMk to clampRange(tV, kMotorMin, kMotorMax) + applyJointMotor pIndex + break + case "jlimiton" + set the uLimitOn of tMk to not (the uLimitOn of tMk is true) + applyJointLimit pIndex + break + case "jlimitlo" + if gJKind[pIndex] is "slider" then put numOr(the uLimitLo of tMk, -100) + pDir * 20 into tV + else put numOr(the uLimitLo of tMk, -45) + pDir * 15 into tV + set the uLimitLo of tMk to tV + applyJointLimit pIndex + break + case "jlimithi" + if gJKind[pIndex] is "slider" then put numOr(the uLimitHi of tMk, 100) + pDir * 20 into tV + else put numOr(the uLimitHi of tMk, 45) + pDir * 15 into tV + set the uLimitHi of tMk to tV + applyJointLimit pIndex + break + case "jspringhz" + put numOr(the uSpringHz of tMk, 0) + pDir * 1 into tV + set the uSpringHz of tMk to clampRange(tV, 0, 15) + applyJointSpring pIndex + break + case "jspringdamp" + put numOr(the uSpringDamp of tMk, 0.5) + pDir * 0.1 into tV + set the uSpringDamp of tMk to clampRange(tV, 0, 1) + applyJointSpring pIndex + break + case "jminlen" + put numOr(the uMinLen of tMk, 0) + pDir * 20 into tV + set the uMinLen of tMk to clampRange(tV, 0, 800) + set the uRangeSet of tMk to true + applyJointRange pIndex + break + case "jmaxlen" + put numOr(the uMaxLen of tMk, 200) + pDir * 20 into tV + set the uMaxLen of tMk to clampRange(tV, 20, 800) + set the uRangeSet of tMk to true + applyJointRange pIndex + break + end switch +end adjustJointProp + +-- Push the marker's stored motor setting to the live joint (kind-specific call). +on applyJointMotor pIndex + local tKind, tJ, tMk, tOn, tSpd + put gJKind[pIndex] into tKind + put gJHandle[pIndex] into tJ + put the long id of graphic ("cb_joint_" & pIndex) into tMk + put (the uMotorOn of tMk is true) into tOn + put numOr(the uMotorSpeed of tMk, gMotorSpeed) into tSpd + switch tKind + case "hinge" + if tOn then b2kMotor tJ, tSpd, kHingeTorque + else b2kMotorOff tJ + break + case "slider" + if tOn then b2kSliderMotor tJ, tSpd + else b2kSliderMotorOff tJ + break + case "wheel" + if tOn then b2kWheelMotor tJ, tSpd, kWheelTorque + else b2kWheelMotorOff tJ + break + end switch + if tOn then put tSpd into gJMotor[pIndex] + else put 0 into gJMotor[pIndex] +end applyJointMotor + +on applyJointLimit pIndex + local tKind, tJ, tMk, tOn, tLo, tHi + put gJKind[pIndex] into tKind + put gJHandle[pIndex] into tJ + put the long id of graphic ("cb_joint_" & pIndex) into tMk + put (the uLimitOn of tMk is true) into tOn + if tKind is "slider" then + put numOr(the uLimitLo of tMk, -100) into tLo + put numOr(the uLimitHi of tMk, 100) into tHi + if tOn then b2kSliderLimit tJ, tLo, tHi + else b2kSliderLimitOff tJ + else if tKind is "hinge" then + put numOr(the uLimitLo of tMk, -45) into tLo + put numOr(the uLimitHi of tMk, 45) into tHi + if tOn then b2kHingeLimit tJ, tLo, tHi + else b2kHingeLimitOff tJ + end if +end applyJointLimit + +on applyJointSpring pIndex + local tKind, tJ, tMk, tHz, tDamp + put gJKind[pIndex] into tKind + put gJHandle[pIndex] into tJ + put the long id of graphic ("cb_joint_" & pIndex) into tMk + put numOr(the uSpringHz of tMk, 0) into tHz + put numOr(the uSpringDamp of tMk, 0.5) into tDamp + switch tKind + case "rope" + if tHz > 0 then b2kSpring tJ, tHz, tDamp + else b2DistanceEnableSpring tJ, false, 0, 0 -- 0 Hz = rigid rope again + break + case "weld" + b2kWeldSpring tJ, tHz, tDamp -- 0 Hz = rigid weld + break + case "wheel" + b2kWheelSpring tJ, tHz, tDamp + break + end switch +end applyJointSpring + +on applyJointRange pIndex + local tJ, tMk + put gJHandle[pIndex] into tJ + put the long id of graphic ("cb_joint_" & pIndex) into tMk + b2kRopeRange tJ, numOr(the uMinLen of tMk, 0), numOr(the uMaxLen of tMk, 200) +end applyJointRange + +-- Click a joint value to type an exact number. +on editJointValue pKey + if gSelJoint is empty then exit editJointValue + set the itemDelimiter to comma + if pKey is among the items of "jmotoron,jlimiton" then exit editJointValue -- toggles only + local tMk + put the long id of graphic ("cb_joint_" & gSelJoint) into tMk + ask ("Set " & jointEditName(pKey) & ":") with jointValueOf(tMk, pKey) + if it is empty or the result is "Cancel" then exit editJointValue + if it is not a number then + put "Please type a number." into gStatus + updateHud + exit editJointValue + end if + applyJointValue gSelJoint, pKey, it + showJointInspector gSelJoint +end editJointValue + +function jointValueOf pMk, pKey + switch pKey + case "jmotorspeed" + return numOr(the uMotorSpeed of pMk, gMotorSpeed) + case "jlimitlo" + return numOr(the uLimitLo of pMk, 0) + case "jlimithi" + return numOr(the uLimitHi of pMk, 0) + case "jspringhz" + return numOr(the uSpringHz of pMk, 0) + case "jspringdamp" + return numOr(the uSpringDamp of pMk, 0.5) + case "jminlen" + return numOr(the uMinLen of pMk, 0) + case "jmaxlen" + return numOr(the uMaxLen of pMk, 200) + end switch + return empty +end jointValueOf + +function jointEditName pKey + switch pKey + case "jmotorspeed" + return "motor speed" + case "jlimitlo" + return "limit minimum" + case "jlimithi" + return "limit maximum" + case "jspringhz" + return "springiness (Hz)" + case "jspringdamp" + return "spring damping (0-1)" + case "jminlen" + return "minimum length (px)" + case "jmaxlen" + return "maximum length (px)" + end switch + return pKey +end jointEditName + +on applyJointValue pIndex, pKey, pVal + local tMk + put the long id of graphic ("cb_joint_" & pIndex) into tMk + switch pKey + case "jmotorspeed" + set the uMotorSpeed of tMk to clampRange(pVal, kMotorMin, kMotorMax) + applyJointMotor pIndex + break + case "jlimitlo" + set the uLimitLo of tMk to pVal + applyJointLimit pIndex + break + case "jlimithi" + set the uLimitHi of tMk to pVal + applyJointLimit pIndex + break + case "jspringhz" + set the uSpringHz of tMk to clampRange(pVal, 0, 15) + applyJointSpring pIndex + break + case "jspringdamp" + set the uSpringDamp of tMk to clampRange(pVal, 0, 1) + applyJointSpring pIndex + break + case "jminlen" + set the uMinLen of tMk to clampRange(pVal, 0, 800) + set the uRangeSet of tMk to true + applyJointRange pIndex + break + case "jmaxlen" + set the uMaxLen of tMk to clampRange(pVal, 20, 800) + set the uRangeSet of tMk to true + applyJointRange pIndex + break + end switch +end applyJointValue + +-- Pack a joint's editable settings as "key=value;…" (per-kind) for Save. +function jointSpecial pIndex + local tKind, tMk, tS + put gJKind[pIndex] into tKind + if tKind is empty then return empty + put the long id of graphic ("cb_joint_" & pIndex) into tMk + put empty into tS + if tKind is "hinge" or tKind is "slider" or tKind is "wheel" then + put addKV(tS, "mon=" & (the uMotorOn of tMk is true)) into tS + put addKV(tS, "msp=" & numOr(the uMotorSpeed of tMk, gMotorSpeed)) into tS + end if + if tKind is "hinge" or tKind is "slider" then + put addKV(tS, "lon=" & (the uLimitOn of tMk is true)) into tS + put addKV(tS, "llo=" & numOr(the uLimitLo of tMk, 0)) into tS + put addKV(tS, "lhi=" & numOr(the uLimitHi of tMk, 0)) into tS + end if + if tKind is "rope" or tKind is "weld" or tKind is "wheel" then + put addKV(tS, "shz=" & numOr(the uSpringHz of tMk, 0)) into tS + put addKV(tS, "sdm=" & numOr(the uSpringDamp of tMk, 0.5)) into tS + end if + if tKind is "rope" and (the uRangeSet of tMk is true) then + put addKV(tS, "rng=true") into tS + put addKV(tS, "rmn=" & numOr(the uMinLen of tMk, 0)) into tS + put addKV(tS, "rmx=" & numOr(the uMaxLen of tMk, 200)) into tS + end if + return tS +end jointSpecial + +-- Restore a joint's editable settings (from Load); empty string = leave defaults. +on applyJointSpecial pIndex, pSpecial + if pSpecial is empty then exit applyJointSpecial + if gJKind[pIndex] is empty then exit applyJointSpecial + local tMk, tV + put the long id of graphic ("cb_joint_" & pIndex) into tMk + put specialValue(pSpecial, "mon") into tV + if tV is not empty then set the uMotorOn of tMk to (tV is "true") + put specialValue(pSpecial, "msp") into tV + if tV is not empty then set the uMotorSpeed of tMk to tV + put specialValue(pSpecial, "lon") into tV + if tV is not empty then set the uLimitOn of tMk to (tV is "true") + put specialValue(pSpecial, "llo") into tV + if tV is not empty then set the uLimitLo of tMk to tV + put specialValue(pSpecial, "lhi") into tV + if tV is not empty then set the uLimitHi of tMk to tV + put specialValue(pSpecial, "shz") into tV + if tV is not empty then set the uSpringHz of tMk to tV + put specialValue(pSpecial, "sdm") into tV + if tV is not empty then set the uSpringDamp of tMk to tV + applyJointMotor pIndex + applyJointLimit pIndex + applyJointSpring pIndex + if specialValue(pSpecial, "rng") is "true" then + set the uRangeSet of tMk to true + put specialValue(pSpecial, "rmn") into tV + if tV is not empty then set the uMinLen of tMk to tV + put specialValue(pSpecial, "rmx") into tV + if tV is not empty then set the uMaxLen of tMk to tV + applyJointRange pIndex + end if +end applyJointSpecial + +-- Defaults for a joint marker's editable settings, by joint kind. +function jointLimitLoDefault pKind + if pKind is "slider" then return -100 + return -45 +end jointLimitLoDefault + +function jointLimitHiDefault pKind + if pKind is "slider" then return 100 + return 45 +end jointLimitHiDefault + +function jointSpringHzDefault pKind + if pKind is "wheel" then return 5 + return 0 +end jointSpringHzDefault + +function jointSpringDampDefault pKind + if pKind is "wheel" then return 0.7 + return 0.5 +end jointSpringDampDefault + function frictionLabel pV if pV is empty then put 0.4 into pV if pV <= 0 then return "none (slippery)" diff --git a/src/box2dxt-kit.livecodescript b/src/box2dxt-kit.livecodescript index 2d1d586..6ca382f 100644 --- a/src/box2dxt-kit.livecodescript +++ b/src/box2dxt-kit.livecodescript @@ -974,6 +974,28 @@ command b2kWheelSpring pJoint, pHertz, pDamping b2WheelEnableSpring pJoint, true, pHertz, pDamping end b2kWheelSpring +-- OFF switches: the b2k… joint wrappers above only ever turn a feature ON, so +-- these let callers disable a motor or limit again (free swing / unbounded slide). +command b2kMotorOff pJoint + if pJoint is not empty and pJoint > 0 then b2RevoluteEnableMotor pJoint, false, 0, 0 +end b2kMotorOff + +command b2kHingeLimitOff pJoint + if pJoint is not empty and pJoint > 0 then b2RevoluteEnableLimit pJoint, false, 0, 0 +end b2kHingeLimitOff + +command b2kSliderMotorOff pJoint + if pJoint is not empty and pJoint > 0 then b2PrismaticEnableMotor pJoint, false, 0, 0 +end b2kSliderMotorOff + +command b2kSliderLimitOff pJoint + if pJoint is not empty and pJoint > 0 then b2PrismaticEnableLimit pJoint, false, 0, 0 +end b2kSliderLimitOff + +command b2kWheelMotorOff pJoint + if pJoint is not empty and pJoint > 0 then b2WheelEnableMotor pJoint, false, 0, 0 +end b2kWheelMotorOff + command b2kRemoveJoint pJoint if pJoint is not empty and pJoint > 0 then b2DestroyJoint pJoint end b2kRemoveJoint From 8f48c5d1c134fb08dce09dd1b877f947d37b0b48 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 00:33:21 +0000 Subject: [PATCH 2/2] Add gravity, rotation, materials, launch, pause/step, motor power & recipes Seven sandbox upgrades that make the Contraption Builder more compelling and customizable. All reuse existing Kit calls and follow the project's LiveCode conventions; nothing existing is removed. Build & control - Part rotation: an Angle control on the Shape tab tilts box/image/ capsule/poly/plate (via b2kMoveTo), so ramps and slopes are buildable. - World gravity: a Gravity button cycles Normal / Low (moon) / Zero-G / Reverse / Sideways (b2kSetGravity); saved with the scene. - Pause / Step: a new b2kStepOnce Kit command plus Pause/Step buttons freeze the sim and advance it one frame at a time. Customize & content - Material presets: a Material control cycles Default / Ice / Rubber / Metal / Wood, setting friction+density+bounce together (the label is derived from current values, so it stays correct after tweaks/reload). - Launch velocity & spin: a new Motion tab sets a per-part initial push (speed + angle) and spin, fired when Run starts (b2kSetVelocity/b2kSpin). - Adjustable motor power: the joint inspector now exposes motor torque/force (previously fixed) for hinge/slider/wheel. - Three new sample contraptions: Catapult, Newton's Cradle, Domino Run. The part inspector now has four tabs (Shape / Physics / Motion / Special). Persistence: angle, launch speed/dir/spin, joint motor power and the world gravity mode all round-trip; every field is append-only, so older CB1/CB2 saves still load and fall back to defaults. Kit: b2kStepOnce added to the embedded and source Kit and documented. https://claude.ai/code/session_01AyoQTVuyLtPMrR5oVW6FFr --- docs/kit-reference.md | 1 + ...box2dxt-contraption-builder.livecodescript | 432 +++++++++++++++++- src/box2dxt-kit.livecodescript | 14 + 3 files changed, 422 insertions(+), 25 deletions(-) diff --git a/docs/kit-reference.md b/docs/kit-reference.md index b38bd74..1d23d3a 100644 --- a/docs/kit-reference.md +++ b/docs/kit-reference.md @@ -58,6 +58,7 @@ b2kStart | `b2kQuickStart [gy]` | One call: world + gravity + card-edge walls + start the loop. | | `b2kStart` / `b2kStop` | Begin / end the simulation loop. | | `b2kPause` / `b2kResume` | Freeze / resume stepping without tearing down. | +| `b2kStepOnce` | Advance exactly one fixed step (even while paused) — drives a Step button. | | `b2kIsRunning()` | True while the loop is stepping (not stopped or paused). | | `b2kAddWalls` | Static walls around the current card edges. | | `b2kAddGround [screenY]` | A static floor across the card (optionally at a given Y). | diff --git a/examples/box2dxt-contraption-builder.livecodescript b/examples/box2dxt-contraption-builder.livecodescript index 358620a..d58fb0f 100644 --- a/examples/box2dxt-contraption-builder.livecodescript +++ b/examples/box2dxt-contraption-builder.livecodescript @@ -1094,6 +1094,20 @@ on b2kStep pGen send ("b2kStep " & sGen) to me in 16 milliseconds end b2kStep +-- Advance exactly one fixed step, even while paused (drives a Step button). +command b2kStepOnce + if not sRunning then exit b2kStepOnce + b2Step sWorld, (1 / 60), sSub + if sDragging then + b2MouseSetTarget sDragJoint, b2kToWorldX(the mouseH), b2kToWorldY(the mouseV) + end if + lock screen + b2kSync + b2kDispatchContacts + if sFrameObj is not empty then dispatch "b2kFrame" to sFrameObj + unlock screen +end b2kStepOnce + command b2kSync local tRef, tBody, tWx, tWy, tDead, d put empty into tDead @@ -1266,7 +1280,8 @@ local gDynParts, gDynDirty -- selection / inspector / onboarding / stress-test bookkeeping local gSelPart, gHoverId, gOnbStep, gStressCount local gSelJoint -- selected joint index (1..gJN), empty if none -local gInspTab -- active inspector tab: shape | physics | special +local gInspTab -- active inspector tab: shape | physics | motion | special +local gGravityMode -- world gravity preset: normal|low|zero|reverse|sideways local gPreRun -- snapshot of the build, taken when Run starts constant kShapeTools = "drag,box,ball,capsule,poly,image,anchor,delete,duplicate" @@ -1305,7 +1320,7 @@ constant kSizeMin = 16, kSizeMax = 240 -- 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) -constant kEntryKeys = "width,height,length,thickness,diameter,size,bounce,density,friction,gravity,lindamp,angdamp,blastradius,blastpower,fanforce,fansize,magforce,magrange" +constant kEntryKeys = "width,height,length,thickness,diameter,size,angle,bounce,density,friction,gravity,lindamp,angdamp,launchspeed,launchdir,spinrate,blastradius,blastpower,fanforce,fansize,magforce,magrange" -- ===================================================================== -- Lifecycle @@ -1342,6 +1357,7 @@ on startCB put empty into gSelPart put empty into gSelJoint put "shape" into gInspTab + put "normal" into gGravityMode put empty into gHoverId put 0 into gStressCount resetSceneState @@ -1500,9 +1516,10 @@ on makeInspector makeLabel "ui_insp_title", tL + 12, tTop + 10, tWide - 12, tTop + 34, "Inspector", 14, "236,238,245", true makeField "ui_insp_body", tL + 12, tTop + 40, tWide - 12, tTop + 118, 11, "190,194,206" -- settings tabs: group a part's many settings so each pane stays short - makeAction "tab_shape", "Shape", tL + 12, tTop + 124, 70, 24 - makeAction "tab_physics", "Physics", tL + 86, tTop + 124, 76, 24 - makeAction "tab_special", "Special", tL + 166, tTop + 124, 70, 24 + makeAction "tab_shape", "Shape", tL + 12, tTop + 124, 50, 24 + makeAction "tab_physics", "Physics", tL + 64, tTop + 124, 58, 24 + makeAction "tab_motion", "Motion", tL + 124, tTop + 124, 52, 24 + makeAction "tab_special", "Special", tL + 178, tTop + 124, 58, 24 -- a clearly-marked, divided settings block so adjustable values stand out makeBar "ui_settings_div", tL + 12, tTop + 156, tWide - 12, tTop + 157, "60,66,80" makeLabel "ui_settings_hdr", tL + 12, tTop + 162, tWide - 12, tTop + 182, "PART SETTINGS", 11, "120,220,150", true @@ -1525,8 +1542,14 @@ on makeInspector makeLabel "ui_lbl_speed", tL + 12, tRy + 2, tWide - 92, tRy + 22, "Motor speed", 11, "168,172,184", false makeAction "spddn", "−", tWide - 84, tRy, 32, 26 makeAction "spdup", "+", tWide - 44, tRy, 32, 26 + add 34 to tRy + makeAction "gravity", "Gravity: Normal", tL + 12, tRy, kInspectorW - 24, 26 + add 32 to tRy + makeAction "pause", "Pause", tL + 12, tRy, 108, 26 + makeAction "step", "Step", tL + 128, tRy, 80, 26 clearPropRows refreshSpeedLabel + refreshGravityLabel end makeInspector -- A prominent accent-green +/- button for adjusting a part's settings. @@ -1883,6 +1906,15 @@ on handleAction pId put "Motor speed: " & gMotorSpeed & " deg/sec." into gStatus refreshSpeedLabel break + case "gravity" + cycleGravity + break + case "pause" + togglePause + break + case "step" + stepOnce + break case "recipes" toggleRecipesMenu break @@ -1916,6 +1948,18 @@ on handleAction pId hideRecipesMenu buildRecipeMagnetCatch break + case "catapult" + hideRecipesMenu + buildRecipeCatapult + break + case "cradle" + hideRecipesMenu + buildRecipeCradle + break + case "dominoes" + hideRecipesMenu + buildRecipeDominoes + break case "stress" hideRecipesMenu stressTest @@ -1939,6 +1983,7 @@ on toggleMode put true into gMotorsRunning armFuses b2kStart -- fresh loop: gravity, motors, mouse drag + armLaunches -- fire each part's initial push/spin put "Run mode — gravity, motors and special parts are live. Drag bodies with the mouse." into gStatus else put "build" into gMode @@ -1948,6 +1993,7 @@ on toggleMode put "Build mode — arrange parts and connect joints, then press Run." into gStatus end if highlightAll + refreshPauseLabel end toggleMode -- The part under a click: prefer the actual control clicked, then fall back to @@ -2435,6 +2481,7 @@ on recordJoint pKind, pJoint, pA, pB, pX, pY, pVis -- default editable joint settings, stored on the marker (like parts' u-props) set the uMotorOn of graphic tG to false set the uMotorSpeed of graphic tG to gMotorSpeed + set the uMotorPower of graphic tG to motorPowerDefault(pKind) set the uLimitOn of graphic tG to false set the uLimitLo of graphic tG to jointLimitLoDefault(pKind) set the uLimitHi of graphic tG to jointLimitHiDefault(pKind) @@ -2702,6 +2749,7 @@ end buildSampleSwing function serializeText local tOut, tP, tI put "CB2" & cr into tOut + put "world" & tab & gGravityMode & cr after tOut repeat for each line tP in gParts if tP is empty then next repeat try @@ -2759,6 +2807,10 @@ function partSpecial pCtrl put addKV(tS, "ldmp=" & (the uLinDamp of pCtrl)) into tS put addKV(tS, "admp=" & (the uAngDamp of pCtrl)) into tS put addKV(tS, "bul=" & (the uBullet of pCtrl is true)) into tS + put addKV(tS, "ang=" & round(b2kAngle(pCtrl))) into tS + put addKV(tS, "lspd=" & (the uLaunchSpeed of pCtrl)) into tS + put addKV(tS, "ldir=" & (the uLaunchDir of pCtrl)) into tS + put addKV(tS, "spin=" & (the uSpinRate of pCtrl)) into tS end if switch tKind case "fan" @@ -2850,6 +2902,7 @@ on rebuildFromText pData local tId, tKind, tX, tY, tW, tH, tBounce, tColor, tFileRef, tSpecial local tIdA, tIdB, tPx, tPy, tMotor, tA, tB, tJSpecial clearAll + put "normal" into gGravityMode put empty into tMap lock screen repeat for each line tLine in pData @@ -2891,11 +2944,16 @@ on rebuildFromText pData if tA is not empty and (tB is not empty or tKind is "hinge") then if connectJoint(tKind, tA, tB, tPx, tPy, tMotor) then applyJointSpecial gJN, tJSpecial end if + else if tRec is "world" then + put item 2 of tLine into gGravityMode + set the itemDelimiter to comma else set the itemDelimiter to comma end if end repeat set the itemDelimiter to comma + applyGravityMode + refreshGravityLabel renderBuild unlock screen end rebuildFromText @@ -3182,6 +3240,16 @@ on applyPartSpecial pCtrl, pSpecial set the uBullet of pCtrl to true b2kSetBullet pCtrl, true end if + put specialValue(pSpecial, "lspd") into tV + if tV is not empty then set the uLaunchSpeed of pCtrl to tV + put specialValue(pSpecial, "ldir") into tV + if tV is not empty then set the uLaunchDir of pCtrl to tV + put specialValue(pSpecial, "spin") into tV + if tV is not empty then set the uSpinRate of pCtrl to tV + put specialValue(pSpecial, "ang") into tV + if tV is not empty and tV is not 0 then + b2kMoveTo pCtrl, item 1 of the loc of pCtrl, item 2 of the loc of pCtrl, tV + end if end if switch tKind case "fan" @@ -3549,21 +3617,21 @@ end showPartInspector function partProps pKind switch pKind case "box" - return "width,height,color,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet" + return "width,height,color,angle,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet,material,launchspeed,launchdir,spinrate" case "image" - return "width,height,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet" + return "width,height,angle,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet,material,launchspeed,launchdir,spinrate" case "ball" - return "diameter,color,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet" + return "diameter,color,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet,material,launchspeed,launchdir,spinrate" case "capsule" - return "length,thickness,color,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet" + return "length,thickness,color,angle,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet,material,launchspeed,launchdir,spinrate" case "poly" - return "size,color,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet" + return "size,color,angle,bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet,material,launchspeed,launchdir,spinrate" case "balloon" return "diameter,color,bounce,density,friction,gravity" case "bomb" return "diameter,color,bounce,density,blastradius,blastpower,bombfuse" case "plate" - return "width,height,color,friction" + return "width,height,color,angle,friction" case "anchor" return "width,height,color" case "fan" @@ -3589,6 +3657,8 @@ function propLabel pCtrl, pKey return "Thickness: " & (the height of pCtrl) & " px" case "diameter" return "Diameter: " & (the width of pCtrl) & " px" + case "angle" + return "Angle: " & round(b2kAngle(pCtrl)) & " deg" case "color" return "Colour (cycle with +/−)" case "bounce" @@ -3611,6 +3681,14 @@ function propLabel pCtrl, pKey return "Air drag (spin): " & numOr(the uAngDamp of pCtrl, 0) case "bullet" return "Fast-collision: " & yesNo(the uBullet of pCtrl is true) + case "material" + return "Material: " & materialNameOf(pCtrl) + case "launchspeed" + return "Launch speed: " & numOr(the uLaunchSpeed of pCtrl, 0) & " px/s" + case "launchdir" + return "Launch angle: " & numOr(the uLaunchDir of pCtrl, 0) & " deg" + case "spinrate" + return "Launch spin: " & numOr(the uSpinRate of pCtrl, 0) & " deg/s" case "blastradius" return "Blast size: " & (the uBlastRadius of pCtrl) case "blastpower" @@ -3681,7 +3759,7 @@ on adjustProp pId end adjustProp on adjustPartProp pCtrl, pKey, pDir - local tB, tD, tF, tNewDir, tMf, tMr, tFr, tG, tBr, tPow, tFs, tIdx, tPal, tCnt, tLd, tAd + local tB, tD, tF, tNewDir, tMf, tMr, tFr, tG, tBr, tPow, tFs, tIdx, tPal, tCnt, tLd, tAd, tLs, tSr switch pKey case "size" if pDir > 0 then resizePart pCtrl, 1.15 @@ -3702,6 +3780,9 @@ on adjustPartProp pCtrl, pKey, pDir case "diameter" setPartDimension pCtrl, "d", (the width of pCtrl) + pDir * kDimStep break + case "angle" + setPartAngle pCtrl, round(b2kAngle(pCtrl)) + pDir * 15 + break case "color" put partSwatches() into tPal put the number of lines of tPal into tCnt @@ -3776,6 +3857,20 @@ on adjustPartProp pCtrl, pKey, pDir set the uBullet of pCtrl to not (the uBullet of pCtrl is true) b2kSetBullet pCtrl, (the uBullet of pCtrl is true) break + case "material" + cycleMaterial pCtrl, pDir + break + case "launchspeed" + put numOr(the uLaunchSpeed of pCtrl, 0) + pDir * 50 into tLs + set the uLaunchSpeed of pCtrl to clampRange(tLs, 0, 1500) + break + case "launchdir" + set the uLaunchDir of pCtrl to ((numOr(the uLaunchDir of pCtrl, 0) + pDir * 15 + 360) mod 360) + break + case "spinrate" + put numOr(the uSpinRate of pCtrl, 0) + pDir * 45 into tSr + set the uSpinRate of pCtrl to clampRange(tSr, -720, 720) + break case "blastradius" put the uBlastRadius of pCtrl into tBr if tBr is empty then put kBombRadius into tBr @@ -3916,16 +4011,18 @@ end swatchIndexOf -- Which settings tab a property key belongs to. function propGroup pKey - if pKey is among the items of "width,height,length,thickness,diameter,size,color" then return "shape" - if pKey is among the items of "bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet" then return "physics" + if pKey is among the items of "width,height,length,thickness,diameter,size,angle,color" then return "shape" + if pKey is among the items of "bounce,density,friction,gravity,fixedrot,bodytype,lindamp,angdamp,bullet,material" then return "physics" + if pKey is among the items of "launchspeed,launchdir,spinrate" then return "motion" return "special" end propGroup -- The tabs that actually have settings for this kind (so empty tabs stay hidden). function partTabs pKind - local tKey, tHasShape, tHasPhys, tHasSpec, tOut + local tKey, tHasShape, tHasPhys, tHasMotion, tHasSpec, tOut put false into tHasShape put false into tHasPhys + put false into tHasMotion put false into tHasSpec set the itemDelimiter to comma repeat for each item tKey in partProps(pKind) @@ -3936,6 +4033,9 @@ function partTabs pKind case "physics" put true into tHasPhys break + case "motion" + put true into tHasMotion + break case "special" put true into tHasSpec break @@ -3944,6 +4044,7 @@ function partTabs pKind put empty into tOut if tHasShape then put "shape" into tOut if tHasPhys then put addItem(tOut, "physics") into tOut + if tHasMotion then put addItem(tOut, "motion") into tOut if tHasSpec then put addItem(tOut, "special") into tOut return tOut end partTabs @@ -3963,7 +4064,7 @@ end selectInspTab on refreshInspTabs pAvail local tT set the itemDelimiter to comma - repeat for each item tT in "shape,physics,special" + repeat for each item tT in "shape,physics,motion,special" if there is not a button ("ui_act_tab_" & tT) then next repeat if tT is among the items of pAvail then set the visible of button ("ui_act_tab_" & tT) to true @@ -3983,7 +4084,7 @@ end refreshInspTabs on hideInspTabs local tT set the itemDelimiter to comma - repeat for each item tT in "shape,physics,special" + repeat for each item tT in "shape,physics,motion,special" if there is a button ("ui_act_tab_" & tT) then set the visible of button ("ui_act_tab_" & tT) to false end repeat end hideInspTabs @@ -4074,6 +4175,8 @@ function currentPropValue pCtrl, pKey case "height" case "thickness" return the height of pCtrl + case "angle" + return round(b2kAngle(pCtrl)) case "bounce" return numOr(the uBounce of pCtrl, 0) case "density" @@ -4086,6 +4189,12 @@ function currentPropValue pCtrl, pKey return numOr(the uLinDamp of pCtrl, 0) case "angdamp" return numOr(the uAngDamp of pCtrl, 0) + case "launchspeed" + return numOr(the uLaunchSpeed of pCtrl, 0) + case "launchdir" + return numOr(the uLaunchDir of pCtrl, 0) + case "spinrate" + return numOr(the uSpinRate of pCtrl, 0) case "blastradius" return numOr(the uBlastRadius of pCtrl, kBombRadius) case "blastpower" @@ -4116,6 +4225,9 @@ on applyPropValue pCtrl, pKey, pVal case "diameter" setPartDimension pCtrl, "d", pVal break + case "angle" + setPartAngle pCtrl, pVal + break case "size" if (the width of pCtrl) >= 1 then resizePart pCtrl, (clampDim(pVal) / (the width of pCtrl)) break @@ -4149,6 +4261,15 @@ on applyPropValue pCtrl, pKey, pVal set the uAngDamp of pCtrl to pVal b2kSetDamping pCtrl, empty, pVal break + case "launchspeed" + set the uLaunchSpeed of pCtrl to clampRange(pVal, 0, 1500) + break + case "launchdir" + set the uLaunchDir of pCtrl to ((pVal + 360) mod 360) + break + case "spinrate" + set the uSpinRate of pCtrl to clampRange(pVal, -720, 720) + break case "blastradius" set the uBlastRadius of pCtrl to clampRange(pVal, 60, 500) break @@ -4274,11 +4395,11 @@ end showJointInspector function jointProps pKind switch pKind case "hinge" - return "jmotoron,jmotorspeed,jlimiton,jlimitlo,jlimithi" + return "jmotoron,jmotorspeed,jmotorpower,jlimiton,jlimitlo,jlimithi" case "slider" - return "jmotoron,jmotorspeed,jlimiton,jlimitlo,jlimithi" + return "jmotoron,jmotorspeed,jmotorpower,jlimiton,jlimitlo,jlimithi" case "wheel" - return "jmotoron,jmotorspeed,jspringhz,jspringdamp" + return "jmotoron,jmotorspeed,jmotorpower,jspringhz,jspringdamp" case "rope" return "jminlen,jmaxlen,jspringhz,jspringdamp" case "weld" @@ -4297,6 +4418,8 @@ function jointPropLabel pMk, pKind, pKey return "Motor: " & onOff(the uMotorOn of pMk is true) case "jmotorspeed" return "Motor speed: " & numOr(the uMotorSpeed of pMk, gMotorSpeed) & tUnit + case "jmotorpower" + return "Motor power: " & numOr(the uMotorPower of pMk, motorPowerDefault(pKind)) case "jlimiton" return "Limits: " & onOff(the uLimitOn of pMk is true) case "jlimitlo" @@ -4335,6 +4458,11 @@ on adjustJointProp pIndex, pKey, pDir set the uMotorSpeed of tMk to clampRange(tV, kMotorMin, kMotorMax) applyJointMotor pIndex break + case "jmotorpower" + put numOr(the uMotorPower of tMk, motorPowerDefault(gJKind[pIndex])) + pDir * 100 into tV + set the uMotorPower of tMk to clampRange(tV, 50, 6000) + applyJointMotor pIndex + break case "jlimiton" set the uLimitOn of tMk to not (the uLimitOn of tMk is true) applyJointLimit pIndex @@ -4378,23 +4506,24 @@ end adjustJointProp -- Push the marker's stored motor setting to the live joint (kind-specific call). on applyJointMotor pIndex - local tKind, tJ, tMk, tOn, tSpd + local tKind, tJ, tMk, tOn, tSpd, tPow put gJKind[pIndex] into tKind put gJHandle[pIndex] into tJ put the long id of graphic ("cb_joint_" & pIndex) into tMk put (the uMotorOn of tMk is true) into tOn put numOr(the uMotorSpeed of tMk, gMotorSpeed) into tSpd + put numOr(the uMotorPower of tMk, motorPowerDefault(tKind)) into tPow switch tKind case "hinge" - if tOn then b2kMotor tJ, tSpd, kHingeTorque + if tOn then b2kMotor tJ, tSpd, tPow else b2kMotorOff tJ break case "slider" - if tOn then b2kSliderMotor tJ, tSpd + if tOn then b2kSliderMotor tJ, tSpd, tPow else b2kSliderMotorOff tJ break case "wheel" - if tOn then b2kWheelMotor tJ, tSpd, kWheelTorque + if tOn then b2kWheelMotor tJ, tSpd, tPow else b2kWheelMotorOff tJ break end switch @@ -4471,6 +4600,8 @@ function jointValueOf pMk, pKey switch pKey case "jmotorspeed" return numOr(the uMotorSpeed of pMk, gMotorSpeed) + case "jmotorpower" + return numOr(the uMotorPower of pMk, 1000) case "jlimitlo" return numOr(the uLimitLo of pMk, 0) case "jlimithi" @@ -4491,6 +4622,8 @@ function jointEditName pKey switch pKey case "jmotorspeed" return "motor speed" + case "jmotorpower" + return "motor power" case "jlimitlo" return "limit minimum" case "jlimithi" @@ -4515,6 +4648,10 @@ on applyJointValue pIndex, pKey, pVal set the uMotorSpeed of tMk to clampRange(pVal, kMotorMin, kMotorMax) applyJointMotor pIndex break + case "jmotorpower" + set the uMotorPower of tMk to clampRange(pVal, 50, 6000) + applyJointMotor pIndex + break case "jlimitlo" set the uLimitLo of tMk to pVal applyJointLimit pIndex @@ -4554,6 +4691,7 @@ function jointSpecial pIndex if tKind is "hinge" or tKind is "slider" or tKind is "wheel" then put addKV(tS, "mon=" & (the uMotorOn of tMk is true)) into tS put addKV(tS, "msp=" & numOr(the uMotorSpeed of tMk, gMotorSpeed)) into tS + put addKV(tS, "mpw=" & numOr(the uMotorPower of tMk, motorPowerDefault(tKind))) into tS end if if tKind is "hinge" or tKind is "slider" then put addKV(tS, "lon=" & (the uLimitOn of tMk is true)) into tS @@ -4582,6 +4720,8 @@ on applyJointSpecial pIndex, pSpecial if tV is not empty then set the uMotorOn of tMk to (tV is "true") put specialValue(pSpecial, "msp") into tV if tV is not empty then set the uMotorSpeed of tMk to tV + put specialValue(pSpecial, "mpw") into tV + if tV is not empty then set the uMotorPower of tMk to tV put specialValue(pSpecial, "lon") into tV if tV is not empty then set the uLimitOn of tMk to (tV is "true") put specialValue(pSpecial, "llo") into tV @@ -4626,6 +4766,174 @@ function jointSpringDampDefault pKind return 0.5 end jointSpringDampDefault +-- ---- Rotation, material presets, launch, gravity, pause ------------- + +-- Re-seat a part at an exact angle (degrees), keeping its position. Joints survive. +on setPartAngle pCtrl, pDeg + local tCx, tCy + 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 + put (((round(pDeg)) mod 360) + 360) mod 360 into pDeg + b2kMoveTo pCtrl, tCx, tCy, pDeg + renderBuild +end setPartAngle + +-- A material is "friction,density,bounce". Presets give parts a quick feel. +function materialPreset pName + switch pName + case "Ice" + return "0.02,0.9,0.05" + case "Rubber" + return "0.9,1,0.85" + case "Metal" + return "0.3,2.6,0.05" + case "Wood" + return "0.5,0.7,0.2" + end switch + return "0.4,1,0.2" -- Default +end materialPreset + +-- Name the preset a part currently matches, or "Custom". +function materialNameOf pCtrl + local tName, tP, tFr, tD, tB + put numOr(the uFriction of pCtrl, 0.4) into tFr + put numOr(the uDensity of pCtrl, 1) into tD + put numOr(the uBounce of pCtrl, 0.2) into tB + set the itemDelimiter to comma + repeat for each item tName in "Default,Ice,Rubber,Metal,Wood" + put materialPreset(tName) into tP + if tFr is item 1 of tP and tD is item 2 of tP and tB is item 3 of tP then return tName + end repeat + return "Custom" +end materialNameOf + +-- Step to the next/previous material preset and apply its values. +on cycleMaterial pCtrl, pDir + local tOrder, tI, tName, tP + put "Default,Ice,Rubber,Metal,Wood" into tOrder + set the itemDelimiter to comma + put itemOffset(materialNameOf(pCtrl), tOrder) + pDir into tI + if tI < 1 then put the number of items of tOrder into tI + if tI > the number of items of tOrder then put 1 into tI + put item tI of tOrder into tName + put materialPreset(tName) into tP + set the uFriction of pCtrl to item 1 of tP + b2kSetFriction pCtrl, item 1 of tP + set the uDensity of pCtrl to item 2 of tP + b2kSetDensity pCtrl, item 2 of tP + set the uBounce of pCtrl to item 3 of tP + b2kSetBounce pCtrl, item 3 of tP +end cycleMaterial + +-- Apply each part's saved launch velocity and spin (called when Run starts). +on armLaunches + local tP, tSpeed, tDir, tSpin + repeat for each line tP in gParts + if tP is empty then next repeat + if not kindHasBody(the uKind of tP) then next repeat + put numOr(the uLaunchSpeed of tP, 0) into tSpeed + put numOr(the uLaunchDir of tP, 0) into tDir + put numOr(the uSpinRate of tP, 0) into tSpin + if tSpeed > 0 then b2kSetVelocity tP, tSpeed * cos(tDir * kPI / 180), - tSpeed * sin(tDir * kPI / 180) + if tSpin is not 0 then b2kSpin tP, tSpin + end repeat +end armLaunches + +-- World gravity presets (metres/sec^2; screen down is negative y). +on applyGravityMode + switch gGravityMode + case "low" + b2kSetGravity 0, -3.5 + break + case "zero" + b2kSetGravity 0, 0 + break + case "reverse" + b2kSetGravity 0, 10 + break + case "sideways" + b2kSetGravity 9, -4 + break + default + b2kSetGravity 0, -10 + end switch +end applyGravityMode + +on cycleGravity + local tOrder, tI + put "normal,low,zero,reverse,sideways" into tOrder + set the itemDelimiter to comma + put itemOffset(gGravityMode, tOrder) + 1 into tI + if tI < 1 or tI > the number of items of tOrder then put 1 into tI + put item tI of tOrder into gGravityMode + applyGravityMode + refreshGravityLabel + put "Gravity: " & gravityModeName(gGravityMode) & "." into gStatus + updateHud +end cycleGravity + +on refreshGravityLabel + if there is a button "ui_act_gravity" then set the label of button "ui_act_gravity" to "Gravity: " & gravityModeName(gGravityMode) +end refreshGravityLabel + +function gravityModeName pMode + switch pMode + case "low" + return "Low (moon)" + case "zero" + return "Zero-G" + case "reverse" + return "Reverse" + case "sideways" + return "Sideways" + end switch + return "Normal" +end gravityModeName + +on togglePause + if gMode is not "run" then + put "Pause works while the simulation is running — press Run first." into gStatus + updateHud + exit togglePause + end if + if b2kIsRunning() then + b2kPause + put "Paused. Use Step to advance one frame, or Pause again to resume." into gStatus + else + b2kResume + put "Resumed." into gStatus + end if + refreshPauseLabel + updateHud +end togglePause + +on stepOnce + if gMode is not "run" then + put "Step works while paused during Run — press Run first." into gStatus + updateHud + exit stepOnce + end if + if b2kIsRunning() then b2kPause -- stepping implies a paused sim + b2kStepOnce + refreshPauseLabel + put "Stepped one frame." into gStatus + updateHud +end stepOnce + +on refreshPauseLabel + if there is not a button "ui_act_pause" then exit refreshPauseLabel + if gMode is "run" and not b2kIsRunning() then set the label of button "ui_act_pause" to "Resume" + else set the label of button "ui_act_pause" to "Pause" +end refreshPauseLabel + +-- Default motor strength by joint kind (torque for hinge/wheel, force for slider). +function motorPowerDefault pKind + if pKind is "hinge" then return kHingeTorque + if pKind is "wheel" then return kWheelTorque + return 1000 +end motorPowerDefault + function frictionLabel pV if pV is empty then put 0.4 into pV if pV <= 0 then return "none (slippery)" @@ -4847,7 +5155,7 @@ on buildRecipesMenu put the width of this card into tWide put the height of this card into tHigh put 280 into tW - put 336 into tH + put 448 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" @@ -4869,6 +5177,12 @@ on buildRecipesMenu makeRecipeBtn "cart", "Motor Cart", tL + 16, tBy, tW - 32 add 34 to tBy makeRecipeBtn "swing", "Pendulum Swing", tL + 16, tBy, tW - 32 + add 34 to tBy + makeRecipeBtn "catapult", "Catapult", tL + 16, tBy, tW - 32 + add 34 to tBy + makeRecipeBtn "cradle", "Newton's Cradle", tL + 16, tBy, tW - 32 + add 34 to tBy + makeRecipeBtn "dominoes", "Domino 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 @@ -4979,6 +5293,74 @@ on buildRecipeMagnetCatch updateHud end buildRecipeMagnetCatch +-- A heavy block drops on one arm of a hinged lever and flings a ball off the other. +on buildRecipeCatapult + if gMode is "run" then toggleMode + clearAll + local tCx, tBaseY, tFulcrum, tArm, tBall, tWeight + put (gArenaL + gArenaR) div 2 into tCx + put gArenaB - 70 into tBaseY + put placeAnchor(tCx, tBaseY) into tFulcrum + put placeBox(tCx, tBaseY - 16, 200, 14, "180,150,90") into tArm + set the uDensity of tArm to 0.6 + b2kSetDensity tArm, 0.6 + get connectJoint("hinge", tArm, tFulcrum, tCx, tBaseY - 16, 0) + put placeBall(tCx - 84, tBaseY - 36, 30, "232,182,92") into tBall + put placeBox(tCx + 78, tBaseY - 172, 54, 54, "214,92,92") into tWeight + set the uDensity of tWeight to 2.6 + b2kSetDensity tWeight, 2.6 + renderBuild + put "Catapult — press Run: the heavy block drops on the right arm and flings the ball off the left." into gStatus + updateHud +end buildRecipeCatapult + +-- Five equal pendulums in a touching row; the raised end ball kicks the chain. +on buildRecipeCradle + if gMode is "run" then toggleMode + clearAll + local tCx, tTopY, tI, tX, tBall, tAnchor, tDiam, tLen + put (gArenaL + gArenaR) div 2 into tCx + put gArenaT + 70 into tTopY + put 38 into tDiam + put 150 into tLen + repeat with tI = 1 to 5 + put tCx + (tI - 3) * tDiam into tX + put placeAnchor(tX, tTopY) into tAnchor + if tI is 1 then + put placeBall(tX - 106, tTopY + 106, tDiam, "120,200,232") into tBall -- raised 45°, same rope length + else + put placeBall(tX, tTopY + tLen, tDiam, "120,200,232") into tBall + end if + set the uBounce of tBall to 0 + b2kSetBounce tBall, 0 + set the uDensity of tBall to 2 + b2kSetDensity tBall, 2 + get connectJoint("rope", tBall, tAnchor, tX, tTopY, 0) + end repeat + renderBuild + put "Newton's Cradle — press Run: the raised ball swings down and kicks the far ball out." into gStatus + updateHud +end buildRecipeCradle + +-- A launched ball rolls into a line of standing dominoes (shows off Launch + Run). +on buildRecipeDominoes + if gMode is "run" then toggleMode + clearAll + local tBaseY, tX, tI, tStartX, tBall + put gArenaB - 6 into tBaseY + put gArenaL + 90 into tStartX + repeat with tI = 1 to 12 + put tStartX + tI * 34 into tX + get placeBox(tX, tBaseY - 34, 12, 60, "206,124,206") + end repeat + put placeBall(tStartX - 16, tBaseY - 40, 34, "232,182,92") into tBall + set the uLaunchSpeed of tBall to 320 + set the uLaunchDir of tBall to 0 + renderBuild + put "Domino Run — press Run: the ball is launched into the line and the dominoes cascade." into gStatus + updateHud +end buildRecipeDominoes + -- Drop a batch of random bodies and run, to find where the frame rate gives out. on stressTest local tI, tX, tY diff --git a/src/box2dxt-kit.livecodescript b/src/box2dxt-kit.livecodescript index 6ca382f..8a2518b 100644 --- a/src/box2dxt-kit.livecodescript +++ b/src/box2dxt-kit.livecodescript @@ -1065,6 +1065,20 @@ on b2kStep pGen send ("b2kStep " & sGen) to me in 16 milliseconds end b2kStep +-- Advance exactly one fixed step, even while paused (drives a Step button). +command b2kStepOnce + if not sRunning then exit b2kStepOnce + b2Step sWorld, (1 / 60), sSub + if sDragging then + b2MouseSetTarget sDragJoint, b2kToWorldX(the mouseH), b2kToWorldY(the mouseV) + end if + lock screen + b2kSync + b2kDispatchContacts + if sFrameObj is not empty then dispatch "b2kFrame" to sFrameObj + unlock screen +end b2kStepOnce + command b2kSync local tRef, tBody, tWx, tWy, tDead, d put empty into tDead