diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72b7c963..880f8049 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,10 +218,15 @@ env: # import of the same name inside that module, and # test_taa, test_dlss and test_ssr each have a # frame() that ae3d.shaders.throng's - # `frame.SCENE_SETS` reads (aether#3084, in the Aether - # session's batch 5). The move, with - # aephysics 6905f03 and those changes, waits on - # branch feat/aether-0838. + # `frame.SCENE_SETS` reads (aether#3084). 0.839.0 + # fixes that, and with @keep on the struct + # parameters a function keeps (0.839's E0902 on + # bind_backend and the VKImage takers) ae3d's own + # code compiles on it; but aephysics 6905f03 does + # not (E0902, broad_phase/module.ae:127, a kept + # DynamicTree), nor does aether-ui's ui.background* + # (aether-ui#221), which the editor builds against. + # The move waits on both, on branch feat/aether-0839. AETHER_REF: v0.834.0 # The aether-ui the editor is built against on the Linux runner. A commit, # not a branch: the editor's CI should say what the editor did, not what diff --git a/docs/motion.md b/docs/motion.md index 6a78400f..6c758fe4 100644 --- a/docs/motion.md +++ b/docs/motion.md @@ -479,11 +479,13 @@ The blended figure takes 15 steps, a quarter second, and no drawn bone turns mor A figure can sit on a seat and lie down on the ground, and get up from either (#825). These are actions at an interaction point, as GTA V's scenario points and Unreal's Smart Objects are: a seat, or a bedroll, and the way the figure faces there. The way down and back up is keyed and planned on its contacts, as the way up from the ground is. The feet stay on the spots they stood on, solved to them every step and pinned there, and the bodies follow the keys through the anchors, so a seat holds them. A game with a clip for the action can give the clip instead, warped to the same point (to come). - **Sitting.** `sit(body, seat, facing)` sits it on `seat` (where its hips' middle comes to rest, on the seat's top), facing `facing`, from POWERED or ANIMATED. The keys follow a person's sit-to-stand (Schenkman et al. 1990) run down and back up: the trunk leans 35° over the feet as the hips go back and down, then sits upright, the hands on the thighs. It is set down by its drawn pelvis and thighs against the seat's own surface under each point of them, sampled once a sit, so a round log holds it as a flat seat does (#833). Each hand rests where its arm reaches with the elbow bent 30° at least, further up the thigh on a high seat (#834). `sit_spot(body, seat, facing)` says where to stand so the shins come out upright: a thigh's length ahead of the seat. `sitting(body)` says it is sitting or sat, `seated(body)` that it has come to sit. -- **Standing up.** `stand_up(body)` leans it forward 42° on the seat, lifts the hips over the feet, and ends in the animation's pose where the feet stand, handed back to the mode it sat down from, as a get-up is. +- **Standing up.** `stand_up(body)` leans it forward 42° on the seat, lifts the hips over the feet, and ends in the animation's pose where the feet stand, handed back to the mode it sat down from, as a get-up is. Sat from ANIMATED, it is handed back into its clip over `BLEND` from the pose last shown, as `set_mode` hands a POWERED figure back. - **Lying down.** `lie_down(body, head)` lays it on its back with its head toward `head`, from where it stands: it squats over its feet, sits back on the ground leaning on its hands behind its hips, and lies back flat, its legs out, its hands by its hips and its elbows on the ground. That is the face-up way up run the other way. `lie_spot(body, middle, head)` says where to stand to lie with the hips at a bedroll's middle. It lies until `get_up(body)`, which rises from its back and ends in the mode it lay down from. `lying_down(body)` says it is lying down or lain. While a figure acts it is in `ACTING` (`acting(body)`), got to as `GETTING_UP` is; `set_mode` ends the action. +A way acted from ANIMATED keys its last pose on the clip as it was when the way began: `physics.ragdoll_keep_animation` takes it then, as a figure let go limp keeps it. The ragdoll had only taken the clip's pose while its bodies drew the figure, and a figure that sat from its clip stood up into a pose of what it had never been given: its drawn hips upside down, facing where it first stood, then turned 3 rad and thrown 66 cm in the step onto its clip as the way set it straight to ANIMATED (#849). + These things about the way's stages came out of measuring the feet and the turns, and apply to every keyed way: - A stage into the animation's pose eases each limb onto its turns by what its solve at that key is short of it, in the bone's own frame: 3° of swing and up to 3° of twist on the box man, because a near-straight knee or elbow has no bend to read its pole from. Eased onto the key's turns in the world instead, while the trunk was still on its way, a figure standing up from a seat drew its ankles 65 mm off their spots in the middle of the stage. A last key planned on its contacts (sat, lying) is not eased at all: its limbs are its solves. @@ -508,6 +510,7 @@ And a key's limbs must be ones its body can take. Lying, the elbows bent toward | sat, the pelvis and thighs over a squared log's top / a round one's, 0.6 m up | 0.00 / 0.03 cm into it | under 1 mm over it, no more than 1 mm into it | | the toes sliding along the ground, sitting down / standing up | 3.1 / 3.8 mm | 5 mm | | the worst turn of any body in a step, sitting and standing, the 0.4 m log / the 0.6 m one | 3.0° / 2.7° | 6° | +| sat from its clip and back onto it: the drawn hips' worst turn / move in a step, and off upright | 1.2° / 1.4 cm, 42° | 6° / 3 cm, 60° | | lying, past the bedroll's edge / under its top | 0 / 0.6 cm | 0 / 1 cm | | up from lying | 3.6 s | 4 s | | the worst turn of any body in a step, lying down and getting up | 5.5° | 6° | diff --git a/docs/physics.md b/docs/physics.md index c77775fa..dcf735f2 100644 --- a/docs/physics.md +++ b/docs/physics.md @@ -225,6 +225,8 @@ The capsule rides a step height (0.35 m) above the feet, so a kerb or a stair lo **In the scene file.** A model's physics record with body `"character"` (the editor's Character kind) is a character: `apply_spec` makes it from the mesh -- a capsule as wide as the mesh's widest half and as tall as it -- with the object's origin `lift` above the feet, the mesh's lowest point, so a model centred on its origin stands with its bottom on the ground. `spec_of` and the scene's attachments write it back the same way. The world keeps a list of its characters; one that nothing has moved yet (no `character_move` from a script) is stood each fixed step, under gravity and on the ground. `character_of(object)` finds an object's character. `tests/test_character.ae`: a 1.8 m box centred on its origin, dropped from 2 m with a character record, stands with its middle at 0.90 m. +**Its own ragdoll.** A character walks through the bodies of its own figure's ragdoll: `motion.on_figure` tells the ragdoll which object it stands for (`ragdoll_set_object`), and a character on that object ignores those bodies, whichever was made first. `character_ignore(controller, ragdoll)` pairs a character with a ragdoll made some other way. Unpaired, the capsule was pushed out of its own figure's bodies: the box man on its Walk, its bodies following the clip, was carried 6 m backwards in 2 s against its input (#851). `tests/test_character_bodies.ae` walks it 2 s: 1.58 m ahead, the capsule made before or after the ragdoll. + ## The scenes **`examples/physics.ae`** runs four of the reference's own scenes rather diff --git a/src/ae3d/motion/module.ae b/src/ae3d/motion/module.ae index b26fc164..90b0f55e 100644 --- a/src/ae3d/motion/module.ae +++ b/src/ae3d/motion/module.ae @@ -1027,6 +1027,7 @@ on_skeleton(e: *Engine, o: *GameObject, sk: *Skeleton) -> *Motion { m = active_ragdoll(r) if m == null { return null } m.object = o as ptr + physics.ragdoll_set_object(r, o) attach(e, m) list.add_raw(motions_for(e).list, m as ptr) watch = engine.script(o, "Motion", m as ptr, null, null) @@ -1204,6 +1205,11 @@ key_drawn(m: *Motion) { key_bodies(m, 0) return } + key_shown(m) +} + +// Key 0: the pose last shown. +key_shown(m: *Motion) { count = physics.ragdoll_bone_count(m.ragdoll) * physics.POSE_STRIDE from = m.now as float[] to = key_at(m, 0) as float[] @@ -1404,6 +1410,7 @@ sit(m: *Motion, seat: Vec3, facing: Vec3) -> bool { way = core.v3_normalize(way) r = m.ragdoll ends_in = m.mode + physics.ragdoll_keep_animation(m.ragdoll) key_bodies(m, 0) read_reach(m, 0) standing = key_at(m, 0) @@ -1520,6 +1527,7 @@ lie_down(m: *Motion, head: Vec3) -> bool { way = core.v3_mul(core.v3_normalize(back), 0.0 - 1.0) r = m.ragdoll ends_in = m.mode + physics.ragdoll_keep_animation(m.ragdoll) up = core.vec3(0.0, 1.0, 0.0) knees = core.v3_add(way, up) key_bodies(m, 0) @@ -1677,6 +1685,7 @@ kneel(m: *Motion, at: Vec3, facing: Vec3) -> bool { way = core.v3_normalize(way) r = m.ragdoll ends_in = m.mode + physics.ragdoll_keep_animation(m.ragdoll) up = core.vec3(0.0, 1.0, 0.0) key_bodies(m, 0) read_reach(m, 0) @@ -3526,6 +3535,7 @@ step_way(m: *Motion, step: float) { physics.ragdoll_restore_animation(m.ragdoll) if !m.blending { m.rise_seconds = m.along } ends_in = m.ends_in + was_blending = m.blending if m.action == ACTION_STAND { leave_seat(m) } m.key_count = 0 m.action = ACTION_NONE @@ -3551,7 +3561,17 @@ step_way(m: *Motion, step: float) { // soles rolling at once after a rise from its back // (tools/plaza_showcase.ae, 20, 24 and 28 walkers, seeds 1 to 4). m.risen_left = RISEN_SECONDS + } else if was_blending { + m.mode = ANIMATED } else { + // Into its clip over BLEND, from the pose shown, as set_mode hands + // a POWERED figure back. Set straight to ANIMATED, it was drawn on + // its clip the next frame wherever the clip had gone while it acted + // (#849). + key_shown(m) + physics.ragdoll_animation_pose(m.ragdoll, key_at(m, 1)) + start_way(m, 2, BLEND, ANIMATED) + m.blending = true m.mode = ANIMATED } return diff --git a/src/ae3d/physics/module.ae b/src/ae3d/physics/module.ae index d470f7f8..eac1893e 100644 --- a/src/ae3d/physics/module.ae +++ b/src/ae3d/physics/module.ae @@ -58,7 +58,7 @@ exports ( hull_of_mesh, mesh_of_mesh, convex_collider_of_model, mesh_collider_of_model, vehicle, vehicle_free, vehicle_drive, vehicle_speed, vehicle_chassis, vehicle_wheel, ragdoll, ragdoll_free, ragdoll_destroy, ragdoll_bone, ragdoll_human, ragdoll_set_velocity, ragdoll_still, ragdoll_kick, - ragdoll_stand, ragdoll_release, ragdoll_is_standing, ragdoll_turn, ragdoll_of_shape, hit_event_count, hit_event, + ragdoll_stand, ragdoll_release, ragdoll_keep_animation, ragdoll_set_object, ragdoll_is_standing, ragdoll_turn, ragdoll_of_shape, hit_event_count, hit_event, ragdoll_dress, ragdoll_dress_named, ragdoll_is_dressed, humanoid_bone, ragdoll_bone_axis, ragdoll_bone_speed, ragdoll_hold_shape, ragdoll_let_go_shape, ragdoll_is_holding_shape, HUMANOID_ENGINE, HUMANOID_MIXAMO, HUMANOID_MIXAMO_BARE, HUMANOID_UNREAL, humanoid_scheme, humanoid_scheme_bone, ragdoll_dress_humanoid, @@ -182,6 +182,7 @@ struct Ragdoll { bones: ptr // list of *GameObject, in human's bone order standing: bool rig: ptr // *Rig, the skinned figure the ragdoll drives, or null + object: ptr // *GameObject the ragdoll stands for (ragdoll_set_object), or null following: bool // the other way round: the rig drives the ragdoll, through its anchors powered: bool // following through its joints' motors within a torque budget, the root free friction: float // newton metres: the joints' motors' friction when not powered @@ -1318,16 +1319,24 @@ ragdoll_stand(r: *Ragdoll, ground: *Rigidbody, hertz: float, damping_ratio: floa r.standing = true } -// The springs and anchors let go: the figure is a ragdoll from here, -// and a rig it wore follows its bodies. -ragdoll_release(r: *Ragdoll) { - if r.standing == false && r.following == false { return } - // Let go while the rig drove it, the rig is the animation's pose: - // kept, as a powered figure keeps it, for whatever brings it back. +// Let go while the rig drove it, the rig is the animation's pose: kept, +// as a powered figure keeps it, for whatever brings it back -- before the +// figure is let go, or shown from keys a way acts from its clip. Not kept +// as a figure on its clip sat down, its way up keyed on the animation's +// pose as it had never been captured, the pelvis upside down and facing +// where the figure first stood (#849). +ragdoll_keep_animation(r: *Ragdoll) { if r.rig != null && r.following && !r.powered { r.written_valid = false capture_animation(r) } +} + +// The springs and anchors let go: the figure is a ragdoll from here, +// and a rig it wore follows its bodies. +ragdoll_release(r: *Ragdoll) { + if r.standing == false && r.following == false { return } + ragdoll_keep_animation(r) human.human_release_align_spring(&r.human) human.human_destroy_anchors(&r.human) unpower(r) @@ -3862,6 +3871,13 @@ ragdoll_free(r: *Ragdoll) { if list.get_raw(p.ragdolls, i) == r as ptr { list.remove(p.ragdolls, i); break } i = i + 1 } + // No controller walks through it any more. + i = 0 + while i < list.size(p.characters) { + c = list.get_raw(p.characters, i) as *CharacterController + if c.ignore == r as ptr { c.ignore = null } + i = i + 1 + } ragdoll_let_go(r) } @@ -5047,7 +5063,16 @@ character_controller(o: *GameObject, radius: float, height: float) -> *Character c.push_force = 400.0 c.solid = false c.near = calloc(CHARACTER_PLANES, 8) - if p != null { list.add_raw(p.characters, c as ptr) } + if p != null { + list.add_raw(p.characters, c as ptr) + // Its figure's own ragdoll, made before it (ragdoll_set_object). + i = 0 + while i < list.size(p.ragdolls) { + r = list.get_raw(p.ragdolls, i) as *Ragdoll + if r.object == o as ptr { c.ignore = r as ptr } + i = i + 1 + } + } return c } @@ -5345,6 +5370,20 @@ character_ignore(c: *CharacterController, r: *Ragdoll) { if c != null { c.ignore = r as ptr } } +// The figure object a ragdoll stands for (motion.on_figure says so): a +// character controller moving that object walks through the ragdoll's +// bodies, as character_ignore has it, whichever of the two was made first. +// Not told, a figure's capsule was pushed out of its own bodies: the box man +// on its clip, its bodies following, was carried 16 m in 2 s as it walked +// (#851). +ragdoll_set_object(r: *Ragdoll, o: *GameObject) { + if r == null { return } + r.object = o as ptr + if o == null { return } + c = character_of(o) + if c != null { c.ignore = r as ptr } +} + // The deepest the capsule is into anything now, in metres: 0 when it // touches nothing or only rests against it. character_overlap(c: *CharacterController) -> float { diff --git a/tests/test_character_bodies.ae b/tests/test_character_bodies.ae index d30fd569..470e4137 100644 --- a/tests/test_character_bodies.ae +++ b/tests/test_character_bodies.ae @@ -10,6 +10,13 @@ // and a walker whose way crossed a lying figure stood 0.3 to 0.4 m up // on it in the plaza; // - and it gets across. +// +// And a figure walking on its clip (locomotion's Walk) with its own +// active ragdoll on it, its bodies following the clip, the game wiring +// nothing between them: its capsule walks through its own bodies, at the +// walk's pace the way it is asked, whichever of the two was made first. +// Pushed out of them, the box man's capsule was carried 16 m in 2 s +// against its input (#851). import ae3d.core import ae3d.engine @@ -18,6 +25,7 @@ import ae3d.loader import ae3d.physics import ae3d.figure import ae3d.motion +import ae3d.locomotion import aephysics.human extern exit(code: int) @@ -46,8 +54,42 @@ facing(m: *Motion) -> Vec3 { return core.v3_normalize(core.vec3(f.x, 0.0, f.z)) } +// A walker on its clip with its own ragdoll, walked 2 s along +z: how far +// its capsule went along the way and in all. +own_walk(controller_first: bool) -> (float, float) { + e = engine.engine_new() + engine.engine_set_fixed_step(e, STEP) + physics.attach(e) + ground = engine.object(e, "Ground", loader.cube(1.0)) + behaviour.object_set_position(ground, core.vec3(0.0, 0.0 - 0.5, 0.0)) + physics.box_collider(ground, core.vec3(40.0, 0.5, 40.0), physics.material(0.8, 0.0)) + o = figure.figure(e, "Walker", "tests/fixtures/gltf/humanoid.glb") + c = null as *CharacterController + if controller_first { c = physics.character_controller(o, 0.3, 1.8) } + m = motion.on_figure(e, o) + if !controller_first { c = physics.character_controller(o, 0.3, 1.8) } + l = locomotion.attach(e, o, c, "Idle", "Walk", "Run") + start = behaviour.object_position(o) + locomotion.set_input(l, core.vec3(0.0, 0.0, 1.0), false) + k = 0 + while k < 120 { + engine.engine_update(e, STEP) + k = k + 1 + } + at = behaviour.object_position(o) + along = at.z - start.z + went = core.v3_len(core.vec3(at.x - start.x, 0.0, at.z - start.z)) + locomotion.locomotion_free(l) + physics.character_free(c) + physics.physics_free(physics.of(e)) + engine.engine_free(e) + figure.figures_free(e) + motion.motion_free(m) + return along, went +} + main() { - println("character_bodies: a character walking across a figure lying in the street") + println("character_bodies: a character walking across a figure lying in the street, and a walker through its own ragdoll (#851)") e = engine.engine_new() engine.engine_set_fixed_step(e, STEP) physics.attach(e) @@ -103,6 +145,11 @@ main() { physics.character_free(c) engine.engine_free(e) motion.motion_free(m) + first_along, first_went = own_walk(true) + then_along, then_went = own_walk(false) + println(" its own ragdoll on it, walked 2 s: the capsule went ${first_along} m ahead (${first_went} in all) made first, ${then_along} m (${then_went}) made after") + check("a walker on its clip with its own ragdoll walks the way it is asked, at a walk (1 to 4 m in 2 s), its capsule made first", first_along > 1.0 && first_went < 4.0) + check("and its capsule made after its ragdoll", then_along > 1.0 && then_went < 4.0) if g_failures == 0 { println("character_bodies: all checks passed (${g_checks})") return diff --git a/tests/test_sit.ae b/tests/test_sit.ae index fbf19e03..51cf3833 100644 --- a/tests/test_sit.ae +++ b/tests/test_sit.ae @@ -23,6 +23,15 @@ // hands on its thighs left its arms all but straight, and standing up // snapped them 7.6 degrees in a step (ae3d#834); // - stood up, it is POWERED again and upright: under 0.1 rad a second on. +// +// And sat from its clip (ANIMATED, the box man on its Idle), it stands up +// onto its clip: ANIMATED again, its drawn hips upright all the way (under +// 60 degrees off it, the lean onto its feet at most 42) and never turning +// more than 6 degrees or moving more than DRAWN_STEP in a step, from the +// first step down to a second on its clip again. The way up was keyed on +// the clip's pose as the ragdoll had never been given it, and the drawn +// hips stood up upside down, facing where the figure first stood, and were +// turned 3 rad and thrown 66 cm in the step onto the clip (ae3d#849). import ae3d.core import ae3d.engine import ae3d.behaviour @@ -30,6 +39,7 @@ import ae3d.loader import ae3d.physics import ae3d.motion import ae3d.figure +import ae3d.skin import aephysics.human import aephysics.dynamics @@ -46,6 +56,7 @@ const LOG_DEPTH = 0.2 // half a squared log's depth const ROUND = 0.2 // a round log's radius, 0.4 m thick const ROUND_HIGH = 0.6 // and its top, on a trestle const REST = 0.001 // m the flesh may be over its seat at the nearest, or into it +const DRAWN_STEP = 0.03 // m the drawn hips may move in a step, sitting or standing var g_checks = 0 var g_failures = 0 @@ -143,7 +154,7 @@ turned_by(a: Quat, b: Quat) -> float { // One sitting: a fresh engine, the box man stood where motion.sit_spot puts // it before the log, sat, a second on the log, and stood up. -sit_on(name: string, round: bool, high: float) { +sit_on(name: string, round: bool, high: float, from_clip: bool) { e = engine.engine_new() engine.engine_set_fixed_step(e, STEP) physics.attach(e) @@ -157,8 +168,12 @@ sit_on(name: string, round: bool, high: float) { engine.engine_free(e) return } + f = figure.figure_of(o) + if from_clip { figure.play(f, "Idle", 1.0, true) } m = motion.on_figure(e, o) - motion.set_mode(m, motion.POWERED) + if !from_clip { motion.set_mode(m, motion.POWERED) } + sk = figure.skeleton(f) + hips = skin.skeleton_bone(sk, skin.skeleton_find(sk, "mixamorig:Hips")) r = m.ragdoll k = 0 while k < 90 { @@ -195,6 +210,16 @@ sit_on(name: string, round: bool, high: float) { } left0 = toe_at(m, human.BONE_FOOT_L) right0 = toe_at(m, human.BONE_FOOT_R) + // The drawn hips, from the frame before it sits: their turn and move in + // each step, and how far off upright they are (the way the hips' up was + // standing, as the figure stood). + core.model_sync_transform(hips) + hips_q = hips.world_rotation + hips_p = hips.world_position + hips_up = core.quat_apply(core.quat_conjugate(hips_q), core.vec3(0.0, 1.0, 0.0)) + drawn_turn = 0.0 + drawn_move = 0.0 + drawn_tilt = 0.0 check("${name}: it sits", motion.sit(m, seat, way) && motion.sitting(m)) worst_turn = 0.0 feet_down = 0.0 @@ -218,6 +243,16 @@ sit_on(name: string, round: bool, high: float) { last[i * 4 + 3] = q.w i = i + 1 } + core.model_sync_transform(hips) + drew = turned_by(hips_q, hips.world_rotation) + if drew > drawn_turn { drawn_turn = drew } + moved = core.v3_len(core.v3_sub(hips.world_position, hips_p)) + if moved > drawn_move { drawn_move = moved } + up = core.quat_apply(hips.world_rotation, hips_up) + tilt = acos(core.clampf(up.y, 0.0 - 1.0, 1.0)) + if tilt > drawn_tilt { drawn_tilt = tilt } + hips_q = hips.world_rotation + hips_p = hips.world_position off = along_ground(toe_at(m, human.BONE_FOOT_L), left0) off_r = along_ground(toe_at(m, human.BONE_FOOT_R), right0) if off_r > off { off = off_r } @@ -244,8 +279,15 @@ sit_on(name: string, round: bool, high: float) { check("${name}: it stands up", standing && up_at >= 0) check("${name}: its feet stay where they stood coming up (under 5 mm along the ground)", standing && feet_up < 0.005) check("${name}: no body turns more than 6 degrees in a step (no pop)", worst_turn < 0.10472) - println(" ${name}, stood up: ${motion.mode_name(motion.mode(m))}, ${motion.tilt(m)} rad a second on") - check("${name}: stood up, it is POWERED again and upright a second on (under 0.1 rad)", up_at >= 0 && motion.mode(m) == motion.POWERED && motion.tilt(m) < 0.1 && !motion.falling(m)) + println(" ${name}, stood up: ${motion.mode_name(motion.mode(m))}, ${motion.tilt(m)} rad a second on; its drawn hips at worst turned ${drawn_turn * 57.29578} degrees and moved ${drawn_move * 100.0} cm in a step, ${drawn_tilt * 57.29578} degrees off upright") + if from_clip { + check("${name}: stood up, it is on its clip again (ANIMATED) a second on", up_at >= 0 && motion.mode(m) == motion.ANIMATED) + check("${name}: its drawn hips stay upright all the way (under 60 degrees off)", drawn_tilt < 1.0472) + check("${name}: its drawn hips never turn more than 6 degrees in a step, onto its clip too (no pop)", drawn_turn < 0.10472) + check("${name}: its drawn hips never move more than 3 cm in a step, onto its clip too", drawn_move < DRAWN_STEP) + } else { + check("${name}: stood up, it is POWERED again and upright a second on (under 0.1 rad)", up_at >= 0 && motion.mode(m) == motion.POWERED && motion.tilt(m) < 0.1 && !motion.falling(m)) + } motion.motions_free(e) physics.physics_free(physics.of(e)) figure.figures_free(e) @@ -253,9 +295,10 @@ sit_on(name: string, round: bool, high: float) { } main() { - println("sit: on a squared log and a round one, and up again (#825, ae3d#833)") - sit_on("a squared log", false, SEAT_HIGH) - sit_on("a round log", true, ROUND_HIGH) + println("sit: on a squared log and a round one, and up again (#825, ae3d#833), and from its clip onto its clip (ae3d#849)") + sit_on("a squared log", false, SEAT_HIGH, false) + sit_on("a round log", true, ROUND_HIGH, false) + sit_on("a squared log, sat from its clip", false, SEAT_HIGH, true) if g_failures > 0 { println("sit: ${g_failures} failure(s) of ${g_checks}") exit(1)