Merge commit '5d9cbdd' into lang/ecs
This commit is contained in:
commit
bcdcc8fe8c
94 changed files with 1184 additions and 329 deletions
10
changes/model-release.md
Normal file
10
changes/model-release.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**A loaded model can be let go of: `model_release(model)`.** Its meshes are freed at once, and
|
||||
each texture once no other model uses it - the glTF loader shares textures between models (one
|
||||
path loaded once, a LOD chain borrowing its LOD0's material), so every cached texture now counts
|
||||
the primitives using it, and the last one frees it and forgets it, with any remembered material
|
||||
that named it. A game can unload a species' kit or a map's models when nothing of them is left,
|
||||
and loading one again afterwards is a fresh load. A scatter layer takes room for what it holds
|
||||
instead of its whole capacity up front (it doubles as it fills, to its cap), which in Maroon Lake
|
||||
was 0.4 GB of never-filled instance arrays. `examples/rendering/release.ludic` is the check.
|
||||
|
|
@ -53,10 +53,10 @@ program UiPointer {
|
|||
let active = down.children[2].bg
|
||||
let mid = said(down)
|
||||
frame(ui_st, hold.x + 5.0, hold.y + 90.0, false, 0.0, false)
|
||||
let off = frame(ui_st, -1.0, -1.0, false, 0.0, false)
|
||||
let off = said(frame(ui_st, -1.0, -1.0, false, 0.0, false))
|
||||
frame(ui_st, -1.0, -1.0, false, 0.0, true)
|
||||
let keyed = said(frame(ui_st, -1.0, -1.0, false, 0.0, true))
|
||||
let up = frame(ui_st, -1.0, -1.0, false, 0.0, false)
|
||||
print(`{ui_pointer_st.log}dragged {int(ui_pointer_st.dragged)} | {mid} active {active} | {said(off)} | {keyed} | {said(up)} {up.children[2].bg}`)
|
||||
print(`{ui_pointer_st.log}dragged {int(ui_pointer_st.dragged)} | {mid} active {active} | {off} | {keyed} | {said(up)} {up.children[2].bg}`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
69
examples/rendering/release.ludic
Normal file
69
examples/rendering/release.ludic
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# release.ludic - a loaded model let go of (model_release): its meshes go at once, a texture it
|
||||
# shares with another model stays until that one goes too, and a model loaded again afterwards is
|
||||
# whole. Prints RELEASE OK.
|
||||
#
|
||||
# bin/ludic build examples/rendering/release.ludic --headless && ./build/release_headless
|
||||
program Release {
|
||||
numbers float
|
||||
import "ludic.render3d/r3d.ludic"
|
||||
|
||||
property Marker { on: int = 1 }
|
||||
model Anchor { Marker }
|
||||
|
||||
function scene_draw(render3d_st: mut Render3dState) -> void { }
|
||||
function scene_draw_casters(render3d_st: mut Render3dState, light_vp: floats) -> void { }
|
||||
function stream_fill(s: Stream, cx: int, cz: int, band: int) -> void { }
|
||||
|
||||
function check(what: string, cond: bool) -> bool {
|
||||
if not cond { print(`release: FAILED - {what}`) }
|
||||
return cond
|
||||
}
|
||||
# whether handle `tex` is still a texture the renderer knows (its recorded kind is set)
|
||||
function tex_live(render3d_st: mut Render3dState, tex: int) -> bool {
|
||||
let o = gpu_tx_at(render3d_st, tex)
|
||||
return tex != 0 and o >= 0 and render3d_st.gpu_tx[o] != 0
|
||||
}
|
||||
|
||||
handler Boot(render3d_st: mut Render3dState) phase Start {
|
||||
spawn Anchor {}
|
||||
r3d_on_draw(render3d_st, fn scene_draw)
|
||||
r3d_on_casters(render3d_st, fn scene_draw_casters)
|
||||
r3d_on_stream_fill(render3d_st, fn stream_fill)
|
||||
r3d_plate_mode(render3d_st, true)
|
||||
let dir = "packages/ludic.lab/plate"
|
||||
render3d_st.r3d_sky_path = `{dir}/sky.hdr`
|
||||
if not r3d_init(render3d_st, 320, 180, "Release") {
|
||||
quit()
|
||||
return
|
||||
}
|
||||
var ok = true
|
||||
# two models of the same file share its two textures
|
||||
let a = gltf_load(render3d_st, dir, "plate.gltf", "plate")
|
||||
let b = gltf_load(render3d_st, dir, "plate.gltf", "plate")
|
||||
ok = check("both plates load", a != null and b != null and len(a.prims) > 0) and ok
|
||||
let diff = a.prims[0].diff
|
||||
ok = check("the plates share their textures", diff != 0 and b.prims[0].diff == diff and render3d_st.gltf_tex_n == 2) and ok
|
||||
model_release(render3d_st, a)
|
||||
ok = check("the first plate is emptied", len(a.prims) == 0) and ok
|
||||
ok = check("what the second still uses stays", tex_live(render3d_st, diff) and render3d_st.gltf_tex_n == 2) and ok
|
||||
model_release(render3d_st, b)
|
||||
ok = check("the last user frees the textures", not tex_live(render3d_st, diff) and render3d_st.gltf_tex_n == 0) and ok
|
||||
ok = check("and its material is forgotten", render3d_st.gltf_mat_n == 0) and ok
|
||||
# one model whose two materials share a texture
|
||||
let p = gltf_load(render3d_st, dir, "probe.gltf", "probe")
|
||||
ok = check("the probe loads its three textures", p != null and render3d_st.gltf_tex_n == 3) and ok
|
||||
model_release(render3d_st, p)
|
||||
ok = check("the probe gives all three back", render3d_st.gltf_tex_n == 0 and render3d_st.gltf_mat_n == 0) and ok
|
||||
# and loading again after a release is a fresh load, whole
|
||||
let c = gltf_load(render3d_st, dir, "plate.gltf", "plate")
|
||||
ok = check("a plate loaded again is whole", c != null and len(c.prims) > 0 and tex_live(render3d_st, c.prims[0].diff) and render3d_st.gltf_tex_n == 2) and ok
|
||||
model_release(render3d_st, c)
|
||||
ok = check("and goes again", render3d_st.gltf_tex_n == 0) and ok
|
||||
if ok { print("RELEASE OK") } else { print("RELEASE FAILED") }
|
||||
quit()
|
||||
}
|
||||
|
||||
handler Present(render3d_st: mut Render3dState) phase Render {
|
||||
r3d_present(render3d_st)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,11 @@ import "ludic.base"
|
|||
import "ludic.physics"
|
||||
program CharacterTest {
|
||||
numbers float
|
||||
function wpose(physics_st: mut PhysicsState, id: int) -> PhysWalk {
|
||||
let p = new PhysWalk
|
||||
if not phys_walker_pose(physics_st, id, p) { return null }
|
||||
return p
|
||||
}
|
||||
|
||||
# the world: x in 4..8 is a deck 45 cm up; z < -10 a cliff; z > 10 a gentle rise; x > 20 the lake
|
||||
function fk_height(x: float, z: float) -> float {
|
||||
|
|
@ -87,7 +92,7 @@ program CharacterTest {
|
|||
function fk_walk(physics_st: mut PhysicsState, character_test_st: mut CharacterTestState, x: float, y: float, z: float, vx: float, vz: float, jump: float, step: float, slope: float, dt: float) -> int {
|
||||
if character_test_st.walker < 0 { character_test_st.walker = fk_jolt(physics_st) }
|
||||
let g = phys_walker_move(physics_st, character_test_st.walker, x, y, z, vx, vz, jump, step, slope, dt)
|
||||
let p = phys_walker_pose(physics_st, character_test_st.walker)
|
||||
let p = wpose(physics_st, character_test_st.walker)
|
||||
character_test_st.wx = p.x
|
||||
character_test_st.wy = p.y
|
||||
character_test_st.wz = p.z
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ import "ludic.nav"
|
|||
| `nav_next_corner(kind, filter, sx, sy, sz, ex, ey, ez) -> bool` | the next corner of that way, read back as the nearest point is (a walker's one question a frame) |
|
||||
| `nav_straight(kind, filter, sx, sy, sz, ex, ez) -> bool` | does the straight line stay walkable |
|
||||
| `nav_random_near(kind, filter, x, y, z, r, seed) -> bool`, `nav_near_x/y/z()` | a reachable point about r away, from the caller's seed |
|
||||
| `nav_asked()`, `nav_found()`, `nav_short()` | paths asked for, found and cut short since the meshes loaded (a game's measure of how often its walkers had a way) |
|
||||
| `nav_asked()`, `nav_found()`, `nav_short()`, `nav_us()` | paths asked for, found and cut short since the meshes loaded, and the microseconds they took (timed in the shim by the OS clock: a game's measure of how often its walkers had a way, and what it cost) |
|
||||
| `nav_area_cost(kind, filter, area, cost)` | how dear a kind of ground is to cross by that filter |
|
||||
|
||||
## Tests
|
||||
|
|
|
|||
BIN
packages/ludic.nav/lib/macos-arm64/libludicnav.dylib
(Stored with Git LFS)
BIN
packages/ludic.nav/lib/macos-arm64/libludicnav.dylib
(Stored with Git LFS)
Binary file not shown.
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.dll
(Stored with Git LFS)
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.dll
(Stored with Git LFS)
Binary file not shown.
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.lib
(Stored with Git LFS)
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.lib
(Stored with Git LFS)
Binary file not shown.
|
|
@ -11,6 +11,7 @@ extern function nvc_nearest(h: pointer, x: float, y: float, z: float, out: point
|
|||
extern function nvc_area_cost(h: pointer, f: int, area: int, cost: float) -> void = "nav_area_cost"
|
||||
extern function nvc_path(h: pointer, f: int, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float, out: pointer, max: int) -> int = "nav_path"
|
||||
extern function nvc_partial(h: pointer) -> int = "nav_partial"
|
||||
extern function nvc_us(h: pointer) -> int = "nav_us"
|
||||
extern function nvc_raycast(h: pointer, f: int, sx: float, sy: float, sz: float, ex: float, ez: float) -> int = "nav_raycast"
|
||||
extern function nvc_random_near(h: pointer, f: int, x: float, y: float, z: float, r: float, seed: int, out: pointer) -> int = "nav_random_near"
|
||||
extern function nvc_stage(size: int) -> pointer = "nav_stage"
|
||||
|
|
|
|||
|
|
@ -22,8 +22,7 @@ NAV_SHIM void nav_area_cost(void *h, int f, int area, float cost) {
|
|||
// the corners of a path from one point to another, at most max of them, into out: the count, or
|
||||
// -1 when either end is off the mesh. One that cannot reach the end stops as near it as the mesh
|
||||
// allows, and nav_partial says so
|
||||
NAV_SHIM int nav_path(void *h, int f, float sx, float sy, float sz, float ex, float ey, float ez, float *out, int max) {
|
||||
Nav *n = static_cast<Nav *>(h);
|
||||
static int nav_path_in(Nav *n, int f, float sx, float sy, float sz, float ex, float ey, float ez, float *out, int max) {
|
||||
float s[3] = {sx, sy, sz}, e[3] = {ex, ey, ez}, so[3], eo[3];
|
||||
dtPolyRef a = nav_poly_at(n, s, so), b = nav_poly_at(n, e, eo);
|
||||
n->partial = 0;
|
||||
|
|
@ -39,6 +38,15 @@ NAV_SHIM int nav_path(void *h, int f, float sx, float sy, float sz, float ex, fl
|
|||
if (dtStatusFailed(n->query->findStraightPath(so, end, n->polys, np, out, nullptr, nullptr, &count, max, 0))) return -1;
|
||||
return count;
|
||||
}
|
||||
NAV_SHIM int nav_path(void *h, int f, float sx, float sy, float sz, float ex, float ey, float ez, float *out, int max) {
|
||||
Nav *n = static_cast<Nav *>(h);
|
||||
long long t0 = nav_now_ns();
|
||||
int r = nav_path_in(n, f, sx, sy, sz, ex, ey, ez, out, max);
|
||||
n->ns += nav_now_ns() - t0;
|
||||
return r;
|
||||
}
|
||||
// microseconds this mesh has spent finding paths
|
||||
NAV_SHIM int nav_us(void *h) { return static_cast<int>(static_cast<Nav *>(h)->ns / 1000); }
|
||||
NAV_SHIM int nav_partial(void *h) { return static_cast<Nav *>(h)->partial; }
|
||||
|
||||
// does the straight line from one point to another stay on the mesh? 1 yes, 0 no, -1 off it
|
||||
|
|
|
|||
|
|
@ -17,7 +17,27 @@
|
|||
#define NAV_SHIM extern "C" __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
extern "C" __declspec(dllimport) int __stdcall QueryPerformanceCounter(long long *);
|
||||
extern "C" __declspec(dllimport) int __stdcall QueryPerformanceFrequency(long long *);
|
||||
#else
|
||||
#include <time.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
// a monotonic clock in nanoseconds, from the OS alone (KERNEL32 on Windows: no C++ runtime to ship)
|
||||
long long nav_now_ns() {
|
||||
#ifdef _WIN32
|
||||
long long c = 0, f = 1;
|
||||
QueryPerformanceCounter(&c);
|
||||
QueryPerformanceFrequency(&f);
|
||||
return (long long)((double)c * 1e9 / (double)f);
|
||||
#else
|
||||
struct timespec t;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
return (long long)t.tv_sec * 1000000000LL + t.tv_nsec;
|
||||
#endif
|
||||
}
|
||||
const int NAV_MAX_POLYS = 512; // polygons a path may cross; a longer one is cut there
|
||||
const int NAV_MAX_NODES = 4096; // the search's open list
|
||||
const int NAV_FILTERS = 8; // costs per kind of ground: one set per kind of walker's taste
|
||||
|
|
@ -30,6 +50,7 @@ struct Nav {
|
|||
float ext[3] = {2.0f, 4.0f, 2.0f}; // how far to look for the mesh around a point
|
||||
dtPolyRef polys[NAV_MAX_POLYS];
|
||||
int partial = 0; // the last path stopped short of its end
|
||||
long long ns = 0; // time spent finding paths, for the measure (17.10)
|
||||
};
|
||||
|
||||
// a navmesh with its query, from params (a set of tiles) or from one tile's data
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@ export function nav_path(nav_st: mut NavState, kind: int, filter: int, sx: float
|
|||
export function nav_asked(nav_st: NavState) -> int { return nav_st.nv_asked }
|
||||
export function nav_found(nav_st: NavState) -> int { return nav_st.nv_found }
|
||||
export function nav_short(nav_st: NavState) -> int { return nav_st.nv_short }
|
||||
# microseconds spent finding those paths, over every loaded mesh
|
||||
export function nav_us(nav_st: NavState) -> int {
|
||||
var us = 0
|
||||
for k in 0 .. NAV_KINDS {
|
||||
let h = nv_mesh(nav_st, k)
|
||||
if h != null { us += nvc_us(h) }
|
||||
}
|
||||
return us
|
||||
}
|
||||
export function nav_partial(nav_st: NavState, kind: int) -> bool {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
return h != null and nvc_partial(h) == 1
|
||||
|
|
|
|||
|
|
@ -124,4 +124,14 @@ program NavTest {
|
|||
nav_reset(nav_st)
|
||||
expect_eq(nav_polygons(nav_st, NAV_SMALL), 0)
|
||||
}
|
||||
|
||||
test "the time paths take is counted, and only while a mesh is loaded" (nav_st: mut NavState) {
|
||||
expect_eq(nav_us(nav_st), 0)
|
||||
expect(meadow(nav_st, NAV_PERSON, 0, true))
|
||||
for i in 0 .. 2000 { nav_path(nav_st, NAV_PERSON, 0, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0) }
|
||||
expect(nav_us(nav_st) > 0)
|
||||
expect(nav_us(nav_st) < 2000000)
|
||||
nav_reset(nav_st)
|
||||
expect_eq(nav_us(nav_st), 0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ by a person's taste.
|
|||
| `npc_pick_name(body)`, `npc_name_used(n)`, `npc_name_clashes()` | names |
|
||||
| `npc_talk(p, player)`, `npc_line_for(p)`, `npc_words(p, line)`, `npc_said()`, `npc_choose(p, line, c, player)`, `npc_talked_today(p)`, `npc_greeting(p)` | talk |
|
||||
| `npc_mirror(on)`, `npc_mirroring()`, `npc_put(slot, id, kind, look, seed, name, hx, hz)`, `npc_heard(slot, x, y, z, yaw, speed, act, stalk)`, `npc_heard_lately(p)`, `npc_mirror_tick(dt)` | a guest's copy of the host's people |
|
||||
| `npc_facts() -> Queue<NpcFact>` | `NPC_F_GREETED` (`player`), `NPC_F_TALKED` (`player`, `line`, `choice`), `NPC_F_ARRIVED` (`act`) |
|
||||
| `npc_facts() -> Queue<NpcFact>` | `NPC_F_GREETED` (`player`), `NPC_F_TALKED` (`player`, `line`, `choice`), `NPC_F_ARRIVED` (`act`), `NPC_F_STUCK` (`how` - `NPC_STUCK_DETOUR`, `_RETRY`, `_GAVE_UP` - at `x`, `z`: the way failed it and it fell back) |
|
||||
| `npc_system() -> System` | `"npc"`, `PH_SIMULATE`, a reset and no tick; nothing saved |
|
||||
|
||||
## Tests
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ export open registry NpcNames of NpcName as NPCN
|
|||
export const NPC_F_GREETED: int = 0 # a player came within notice
|
||||
export const NPC_F_TALKED: int = 1 # a player talked (line, choice -1 or the choice taken)
|
||||
export const NPC_F_ARRIVED: int = 2 # got where it was walking, and began the act
|
||||
export const NPC_F_STUCK: int = 3 # the way failed it and it fell back (`how`, at `x`, `z`)
|
||||
|
||||
export property NpcFact {
|
||||
what: int = 0
|
||||
|
|
@ -141,4 +142,7 @@ export property NpcFact {
|
|||
line: int = -1
|
||||
choice: int = -1
|
||||
act: int = -1
|
||||
how: int = 0 # NPC_STUCK_*
|
||||
x: float = 0.0
|
||||
z: float = 0.0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ program NpcTest {
|
|||
expect_eq(p.act, NPCA_SIT)
|
||||
}
|
||||
|
||||
test "nobody walks into the lake" (npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
|
||||
test "nobody walks into the lake, and turning back from it is told as a fallback" (npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
|
||||
let p = one(npc_st, npc_tests_fake_st, NPCK_HIKER)
|
||||
npc_go(npc_st, p, -80.0, 0.0, NPCA_SIT, 30.0)
|
||||
var west = 0.0
|
||||
|
|
@ -123,6 +123,12 @@ program NpcTest {
|
|||
west = Math.min(west, p.x)
|
||||
}
|
||||
expect(west > -50.5)
|
||||
var fell = 0
|
||||
let fs = q_drain(npc_facts(npc_st))
|
||||
for i in 0 .. len(fs) {
|
||||
if fs[i].what == NPC_F_STUCK and fs[i].x > -52.0 { fell += 1 }
|
||||
}
|
||||
expect(fell >= 1)
|
||||
}
|
||||
|
||||
test "a step's spot is where its hours say: the shore by day, home by night" (npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
|
||||
|
|
|
|||
|
|
@ -78,5 +78,7 @@ program NpcWayTest {
|
|||
expect_eq(p.act, NPCA_SIT)
|
||||
expect(npc_d(p.x, p.z, 38.0, -25.0) < 1.3)
|
||||
expect(way_test_st.asked > 5)
|
||||
let fs = q_drain(npc_facts(npc_st))
|
||||
for i in 0 .. len(fs) { expect(fs[i].what != NPC_F_STUCK) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export function npc_walk(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void
|
|||
p.stall = p.stall + dt
|
||||
if p.stall > p.stall_max { p.stall_max = p.stall }
|
||||
if p.detours < 10 and np_detour(p, want) {
|
||||
np_stuck_fact(npc_st, p, NPC_STUCK_DETOUR)
|
||||
p.detours += 1
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
# way.ludic - where a person walks when the straight line will not do: the world's way (NpcWorld.way,
|
||||
# a navmesh by a person's taste in Maroon Lake), and without one a step round what is in the way,
|
||||
# and giving the target up after three tries
|
||||
# and giving the target up after three tries - each of those a NPC_F_STUCK fact, so the fallback is seen
|
||||
export const NPC_STUCK_DETOUR: int = 0 # a step round what was in front of it
|
||||
export const NPC_STUCK_RETRY: int = 1 # no closer for twenty seconds, or no step round: planned again
|
||||
export const NPC_STUCK_GAVE_UP: int = 2 # the third of those: the target given up
|
||||
function np_stuck_fact(npc_st: NpcState, p: NpcPerson, how: int) -> void {
|
||||
let f = np_fact(npc_st, NPC_F_STUCK, p, -1)
|
||||
f.how = how
|
||||
f.x = p.x
|
||||
f.z = p.z
|
||||
}
|
||||
|
||||
# the next corner toward the target, asked again every half second, on reaching it, or for a new target
|
||||
const NP_WAY_EVERY: float = 0.5
|
||||
|
|
@ -39,7 +48,11 @@ function np_stuck(npc_st: mut NpcState, p: NpcPerson) -> void {
|
|||
p.fails += 1
|
||||
p.via_on = false
|
||||
if p.fails >= 3 {
|
||||
np_stuck_fact(npc_st, p, NPC_STUCK_GAVE_UP)
|
||||
p.fails = 0
|
||||
npc_wander(npc_st, p)
|
||||
} else { npc_plan(npc_st, p) }
|
||||
} else {
|
||||
np_stuck_fact(npc_st, p, NPC_STUCK_RETRY)
|
||||
npc_plan(npc_st, p)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ import "ludic.physics"
|
|||
|
||||
## The API
|
||||
|
||||
A query fills a record the caller keeps (`p`, `h`) and allocates nothing: it is asked every frame, and
|
||||
Ludic gives no allocation back.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `phys_open(st, max_bodies, threads) -> bool`, `phys_close(st)`, `phys_is_open(st)` | a world, and everything in it let go |
|
||||
|
|
@ -34,15 +37,15 @@ import "ludic.physics"
|
|||
| `phys_ground_add`, `phys_static_add`, `phys_kinematic_add`, `phys_body_add(..., mass)` | bodies: the ground, still things, what the game moves (the player), what falls and floats |
|
||||
| `phys_settle(st)` | after adding many still things, before the first query |
|
||||
| `phys_remove(st, id)`, `phys_count(st)` | gone for good |
|
||||
| `phys_pose(st, id) -> PhysPose`, `phys_yaw_of(p)` | position, rotation, velocity |
|
||||
| `phys_pose(st, id, p) -> bool`, `phys_yaw_of(p)` | position, rotation, velocity |
|
||||
| `phys_place`, `phys_impulse`, `phys_set_velocity`, `phys_set_kinematic_target`, `phys_awake` | moving a body |
|
||||
| `phys_ray(st, o, d, mask) -> PhysHit` | the first thing along a ray |
|
||||
| `phys_ray(st, o, d, mask, h) -> bool` | the first thing along a ray |
|
||||
| `phys_top_at(st, x, z, r, from_y, depth, mask) -> float` | what a foot would land on (`CharacterGround.top_at`), `PHYS_NONE` if nothing |
|
||||
| `phys_push(st, x, z, r, y0, y1, mask) -> PhysPush` | the move that stands an upright body clear (`CharacterGround.push`) |
|
||||
| `phys_push(st, x, z, r, y0, y1, mask, p) -> int` | the move that stands an upright body clear (`CharacterGround.push`) |
|
||||
| `phys_resolve(st, x, z, r, y0, y1, mask) -> bool`, `phys_resolved_x` / `_z` | pushed clear in two passes, the place kept (`CharacterGround.push`, `pushed_x` / `_z`); a thing wholly under the feet or over the head is not in the way |
|
||||
| `phys_step_top(st, x, z, r, feet, reach, mask) -> float` | the highest top a foot could step up to, anything taller than `feet + reach` left out as a wall |
|
||||
| `phys_overlap(st, x, y, z, r, mask) -> []int` | the bodies a ball touches |
|
||||
| `phys_walker_add(st, r, h, x, y, z, max_slope, mass)`, `phys_walker_step(st, id, dt, vx, vz, jump) -> ground`, `phys_walker_pose -> PhysWalk`, `phys_walker_place`, `phys_walker_limits(step_up, stick)`, `phys_walker_remove` | a walking body (Jolt's CharacterVirtual): gravity, landing, steps, slopes, following the ground down; it shoves dynamic things and rays find its `body` |
|
||||
| `phys_overlap(st, x, y, z, r, mask) -> int`, `phys_overlap_id(st, i)` | how many bodies a ball touches, and each one |
|
||||
| `phys_walker_add(st, r, h, x, y, z, max_slope, mass)`, `phys_walker_step(st, id, dt, vx, vz, jump) -> ground`, `phys_walker_pose(st, id, p) -> bool`, `phys_walker_place`, `phys_walker_limits(step_up, stick)`, `phys_walker_remove` | a walking body (Jolt's CharacterVirtual): gravity, landing, steps, slopes, following the ground down; it shoves dynamic things and rays find its `body` |
|
||||
| `phys_force`, `phys_torque`, `phys_angular_impulse`, `phys_spin_y`, `phys_set_pose(st, id, pose)` | an oar's stroke and a turn; a guest's copy put where the host said |
|
||||
| `phys_float(st, id, buoyancy, linear_drag, angular_drag)`, `phys_sink`, `phys_floating` | a body buoyed each step against `PhysWater` and carried by its current |
|
||||
| `phys_facts(st)` | `PhysFact`: `PHYS_HIT` (two bodies met at `ph_hard` m/s or more), `PHYS_SPLASH` (a floating body reached water) |
|
||||
|
|
|
|||
|
|
@ -43,17 +43,16 @@ export function phys_remove(physics_st: mut PhysicsState, id: int) -> void {
|
|||
jph_body_remove(physics_st.ph_world, id)
|
||||
}
|
||||
|
||||
# where a body is, how it is turned and how fast it goes; null when there is no such body
|
||||
export function phys_pose(physics_st: mut PhysicsState, id: int) -> PhysPose {
|
||||
if physics_st.ph_world == null { return null }
|
||||
ph_buffers()
|
||||
if jph_body_read(physics_st.ph_world, id, physics_st.ph_vals) != 0 { return null }
|
||||
# where a body is, how it is turned and how fast it goes, into the caller's own record (a query asked
|
||||
# every frame allocates nothing: there is no GC); false, and p untouched, when there is no such body
|
||||
export function phys_pose(physics_st: mut PhysicsState, id: int, p: PhysPose) -> bool {
|
||||
if physics_st.ph_world == null { return false }
|
||||
if jph_body_read(physics_st.ph_world, id, physics_st.ph_vals) != 0 { return false }
|
||||
let v = physics_st.ph_vals
|
||||
let p = new PhysPose
|
||||
p.x = v[0]; p.y = v[1]; p.z = v[2]
|
||||
p.qx = v[3]; p.qy = v[4]; p.qz = v[5]; p.qw = v[6]
|
||||
p.vx = v[7]; p.vy = v[8]; p.vz = v[9]
|
||||
return p
|
||||
return true
|
||||
}
|
||||
|
||||
# the heading a pose's rotation gives, in radians about y
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ export function phys_hull_add(physics_st: mut PhysicsState, x: float, y: float,
|
|||
# a stroke of `forward` newtons along the bow and a turn of `turn` newton-metres about y, over the
|
||||
# next step
|
||||
export function phys_row(physics_st: mut PhysicsState, id: int, forward: float, turn: float) -> void {
|
||||
let p = phys_pose(physics_st, id)
|
||||
if p == null { return }
|
||||
let yaw = phys_yaw_of(p)
|
||||
if physics_st.ph_world == null or jph_body_read(physics_st.ph_world, id, physics_st.ph_vals) != 0 { return }
|
||||
let v = physics_st.ph_vals
|
||||
let yaw = Math.atan2(2.0 * (v[6] * v[4] + v[3] * v[5]), 1.0 - 2.0 * (v[4] * v[4] + v[3] * v[3]))
|
||||
phys_force(physics_st, id, -Math.sin(yaw) * forward, 0.0, -Math.cos(yaw) * forward)
|
||||
phys_torque(physics_st, id, 0.0, turn, 0.0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,18 +12,19 @@ export property PhysHit {
|
|||
nz: float = 0.0
|
||||
}
|
||||
|
||||
# a ray from (ox, oy, oz) along (dx, dy, dz), whose length is the reach
|
||||
export function phys_ray(physics_st: mut PhysicsState, ox: float, oy: float, oz: float, dx: float, dy: float, dz: float, mask: int) -> PhysHit {
|
||||
let h = new PhysHit
|
||||
if physics_st.ph_world == null { return h }
|
||||
ph_buffers()
|
||||
# a ray from (ox, oy, oz) along (dx, dy, dz), whose length is the reach, into the caller's record;
|
||||
# true when it hit something (h.body -1 and h.fraction 1 when not)
|
||||
export function phys_ray(physics_st: mut PhysicsState, ox: float, oy: float, oz: float, dx: float, dy: float, dz: float, mask: int, h: PhysHit) -> bool {
|
||||
h.body = -1
|
||||
h.fraction = 1.0
|
||||
if physics_st.ph_world == null { return false }
|
||||
h.body = jph_ray(physics_st.ph_world, ox, oy, oz, dx, dy, dz, mask, physics_st.ph_vals)
|
||||
if h.body == -1 { return h }
|
||||
if h.body == -1 { return false }
|
||||
let v = physics_st.ph_vals
|
||||
h.fraction = v[0]
|
||||
h.x = v[1]; h.y = v[2]; h.z = v[3]
|
||||
h.nx = v[4]; h.ny = v[5]; h.nz = v[6]
|
||||
return h
|
||||
return true
|
||||
}
|
||||
|
||||
# a ball r across let down from from_y at most depth metres: the height of what it lands on, or
|
||||
|
|
@ -45,15 +46,13 @@ export function phys_step_top(physics_st: PhysicsState, x: float, z: float, r: f
|
|||
return y
|
||||
}
|
||||
|
||||
# the bodies a ball at (x, y, z) touches, in the order Jolt sorts them
|
||||
export function phys_overlap(physics_st: mut PhysicsState, x: float, y: float, z: float, r: float, mask: int) -> []int {
|
||||
let out = new []int
|
||||
if physics_st.ph_world == null { return out }
|
||||
ph_buffers()
|
||||
let n = jph_overlap(physics_st.ph_world, x, y, z, r, mask, physics_st.ph_ids, PH_CAP)
|
||||
for i in 0 .. n { push(out, physics_st.ph_ids[i]) }
|
||||
return out
|
||||
# how many bodies a ball at (x, y, z) touches, in the order Jolt sorts them; phys_overlap_id(i)
|
||||
# reads them back until the next query
|
||||
export function phys_overlap(physics_st: mut PhysicsState, x: float, y: float, z: float, r: float, mask: int) -> int {
|
||||
if physics_st.ph_world == null { return 0 }
|
||||
return jph_overlap(physics_st.ph_world, x, y, z, r, mask, physics_st.ph_ids, PH_CAP)
|
||||
}
|
||||
export function phys_overlap_id(physics_st: PhysicsState, i: int) -> int { return physics_st.ph_ids[i] }
|
||||
|
||||
export property PhysPush {
|
||||
x: float = 0.0 # how far to move in x and z to stand clear
|
||||
|
|
@ -63,14 +62,18 @@ export property PhysPush {
|
|||
|
||||
# an upright body r wide from y0 to y1 at (x, z): the move that stands it clear - what
|
||||
# CharacterGround.push asks
|
||||
export function phys_push(physics_st: mut PhysicsState, x: float, z: float, r: float, y0: float, y1: float, mask: int) -> PhysPush {
|
||||
let p = new PhysPush
|
||||
if physics_st.ph_world == null { return p }
|
||||
ph_buffers()
|
||||
p.n = jph_push(physics_st.ph_world, x, z, r, y0, y1, mask, physics_st.ph_vals)
|
||||
export function phys_push(physics_st: mut PhysicsState, x: float, z: float, r: float, y0: float, y1: float, mask: int, p: PhysPush) -> int {
|
||||
p.n = ph_push(physics_st, x, z, r, y0, y1, mask)
|
||||
p.x = physics_st.ph_vals[0]
|
||||
p.z = physics_st.ph_vals[1]
|
||||
return p
|
||||
return p.n
|
||||
}
|
||||
# how many things are in the way, the move clear in ph_vals[0], [1]
|
||||
function ph_push(physics_st: mut PhysicsState, x: float, z: float, r: float, y0: float, y1: float, mask: int) -> int {
|
||||
physics_st.ph_vals[0] = 0.0
|
||||
physics_st.ph_vals[1] = 0.0
|
||||
if physics_st.ph_world == null { return 0 }
|
||||
return jph_push(physics_st.ph_world, x, z, r, y0, y1, mask, physics_st.ph_vals)
|
||||
}
|
||||
|
||||
# a body r wide from y0 to y1 at (x, z) moved clear of what it stands in, two passes over (a push out
|
||||
|
|
@ -82,10 +85,9 @@ export function phys_resolve(physics_st: mut PhysicsState, x: float, z: float, r
|
|||
var pz = z
|
||||
var moved = false
|
||||
for pass in 0 .. 2 {
|
||||
let p = phys_push(physics_st, px, pz, r, y0, y1, mask)
|
||||
if p.n > 0 {
|
||||
px += p.x
|
||||
pz += p.z
|
||||
if ph_push(physics_st, px, pz, r, y0, y1, mask) > 0 {
|
||||
px += physics_st.ph_vals[0]
|
||||
pz += physics_st.ph_vals[1]
|
||||
moved = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ import "ludic.physics"
|
|||
import "ludic.base"
|
||||
program HullShapeTest {
|
||||
numbers float
|
||||
function ray(physics_st: mut PhysicsState, ox: float, oy: float, oz: float, dx: float, dy: float, dz: float, mask: int) -> PhysHit {
|
||||
let h = new PhysHit
|
||||
phys_ray(physics_st, ox, oy, oz, dx, dy, dz, mask, h)
|
||||
return h
|
||||
}
|
||||
|
||||
# a cube two metres across, its centre two metres along x from its origin
|
||||
function cube(physics_st: mut PhysicsState) -> int {
|
||||
|
|
@ -15,7 +20,7 @@ program HullShapeTest {
|
|||
}
|
||||
return phys_convex(physics_st, v, 8)
|
||||
}
|
||||
function down(physics_st: mut PhysicsState, x: float, z: float) -> PhysHit { return phys_ray(physics_st, x, 10.0, z, 0.0, -20.0, 0.0, PHYS_M_ALL) }
|
||||
function down(physics_st: mut PhysicsState, x: float, z: float) -> PhysHit { return ray(physics_st, x, 10.0, z, 0.0, -20.0, 0.0, PHYS_M_ALL) }
|
||||
|
||||
test "a hull stands where its points are, and too few points are no shape" (physics_st: mut PhysicsState) {
|
||||
expect(phys_open(physics_st, 16, 1))
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ import "ludic.physics"
|
|||
import "ludic.base"
|
||||
program MotionTest {
|
||||
numbers float
|
||||
function pose(physics_st: mut PhysicsState, id: int) -> PhysPose {
|
||||
let p = new PhysPose
|
||||
if not phys_pose(physics_st, id, p) { return null }
|
||||
return p
|
||||
}
|
||||
|
||||
function lake_h(x: float, z: float) -> float {
|
||||
if x > 20.0 { return 0.0 }
|
||||
|
|
@ -45,7 +50,7 @@ program MotionTest {
|
|||
let crate = phys_body_add(physics_st, phys_box(physics_st, 0.5, 0.25, 0.5), 30.0, 2.0, 0.0, 0.0, 60.0)
|
||||
phys_float(physics_st, crate, 1.2, 2.0, 2.0)
|
||||
sim(physics_st, 8.0)
|
||||
let p = phys_pose(physics_st, crate)
|
||||
let p = pose(physics_st, crate)
|
||||
expect(p.y > -0.4)
|
||||
expect(p.y < 0.4)
|
||||
expect(Math.abs(p.vy) < 0.2)
|
||||
|
|
@ -55,7 +60,7 @@ program MotionTest {
|
|||
phys_sink(physics_st, crate)
|
||||
expect(not phys_floating(physics_st, crate))
|
||||
sim(physics_st, 4.0)
|
||||
expect(phys_pose(physics_st, crate).y < -2.5)
|
||||
expect(pose(physics_st, crate).y < -2.5)
|
||||
phys_close(physics_st)
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +92,7 @@ program MotionTest {
|
|||
phys_set_kinematic_target(physics_st, me, 2.0 + float(i + 1) * 0.03, 0.95, 0.0, 0.0)
|
||||
phys_step(physics_st)
|
||||
}
|
||||
expect(phys_pose(physics_st, crate).x > 5.0)
|
||||
expect(pose(physics_st, crate).x > 5.0)
|
||||
phys_close(physics_st)
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +119,7 @@ program MotionTest {
|
|||
for i in 0 .. 12 { last = phys_body_add(physics_st, b, 3.0 + float(i % 3) * 0.3, 2.0 + float(i) * 0.7, float(i % 2) * 0.2, float(i) * 0.4, 8.0) }
|
||||
phys_impulse(physics_st, last, 20.0, 0.0, 5.0)
|
||||
sim(physics_st, 4.0)
|
||||
let p = phys_pose(physics_st, last)
|
||||
let p = pose(physics_st, last)
|
||||
phys_close(physics_st)
|
||||
return p
|
||||
}
|
||||
|
|
@ -132,21 +137,21 @@ program MotionTest {
|
|||
world(physics_st)
|
||||
let b = phys_hull_add(physics_st, 40.0, 0.5, 0.0, 1.5707963, 0.6, 0.25, 1.9, 180.0, 4.0, 1.0)
|
||||
sim(physics_st, 4.0)
|
||||
let p0 = phys_pose(physics_st, b)
|
||||
let p0 = pose(physics_st, b)
|
||||
expect(p0.y > -0.2)
|
||||
expect(p0.y < 0.2)
|
||||
for i in 0 .. 240 {
|
||||
phys_row(physics_st, b, 420.0, 0.0)
|
||||
phys_step(physics_st)
|
||||
}
|
||||
let p1 = phys_pose(physics_st, b)
|
||||
let p1 = pose(physics_st, b)
|
||||
expect(p1.x < 34.0)
|
||||
expect(phys_bow_speed(p1) > 1.0)
|
||||
for i in 0 .. 120 {
|
||||
phys_row(physics_st, b, 0.0, 520.0)
|
||||
phys_step(physics_st)
|
||||
}
|
||||
expect(Math.abs(phys_yaw_of(phys_pose(physics_st, b)) - phys_yaw_of(p1)) > 0.5)
|
||||
expect(Math.abs(phys_yaw_of(pose(physics_st, b)) - phys_yaw_of(p1)) > 0.5)
|
||||
phys_close(physics_st)
|
||||
world(physics_st)
|
||||
let c = phys_hull_add(physics_st, 24.0, 0.5, 0.0, 1.5707963, 0.6, 0.25, 1.9, 180.0, 4.0, 1.0)
|
||||
|
|
@ -154,7 +159,7 @@ program MotionTest {
|
|||
phys_row(physics_st, c, 420.0, 0.0)
|
||||
phys_step(physics_st)
|
||||
}
|
||||
expect(phys_pose(physics_st, c).x > 19.5)
|
||||
expect(pose(physics_st, c).x > 19.5)
|
||||
phys_close(physics_st)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,21 @@ import "ludic.physics"
|
|||
import "ludic.base"
|
||||
program PhysicsTest {
|
||||
numbers float
|
||||
function pose(physics_st: mut PhysicsState, id: int) -> PhysPose {
|
||||
let p = new PhysPose
|
||||
if not phys_pose(physics_st, id, p) { return null }
|
||||
return p
|
||||
}
|
||||
function ray(physics_st: mut PhysicsState, ox: float, oy: float, oz: float, dx: float, dy: float, dz: float, mask: int) -> PhysHit {
|
||||
let h = new PhysHit
|
||||
phys_ray(physics_st, ox, oy, oz, dx, dy, dz, mask, h)
|
||||
return h
|
||||
}
|
||||
function push_of(physics_st: mut PhysicsState, x: float, z: float, r: float, y0: float, y1: float, mask: int) -> PhysPush {
|
||||
let p = new PhysPush
|
||||
phys_push(physics_st, x, z, r, y0, y1, mask, p)
|
||||
return p
|
||||
}
|
||||
|
||||
# the lake: everything east of x = 20, its surface at y = 0, drifting north at 0.5 m/s
|
||||
function lake_h(x: float, z: float) -> float {
|
||||
|
|
@ -38,15 +53,15 @@ program PhysicsTest {
|
|||
let id = phys_body_add(physics_st, b, 0.0, 4.0, 0.0, 0.0, 20.0)
|
||||
expect(id != -1)
|
||||
sim(physics_st, 3.0)
|
||||
let p = phys_pose(physics_st, id)
|
||||
let p = pose(physics_st, id)
|
||||
expect(p != null)
|
||||
expect(Math.abs(p.y - 0.5) < 0.05)
|
||||
expect(Math.abs(p.vy) < 0.05)
|
||||
let h = phys_ray(physics_st, 0.0, 10.0, 0.0, 0.0, -20.0, 0.0, PHYS_M_ALL)
|
||||
let h = ray(physics_st, 0.0, 10.0, 0.0, 0.0, -20.0, 0.0, PHYS_M_ALL)
|
||||
expect_eq(h.body, id)
|
||||
expect(Math.abs(h.y - 1.0) < 0.05)
|
||||
expect(h.ny > 0.9)
|
||||
let g = phys_ray(physics_st, 5.0, 10.0, 5.0, 0.0, -20.0, 0.0, PHYS_M_ALL)
|
||||
let g = ray(physics_st, 5.0, 10.0, 5.0, 0.0, -20.0, 0.0, PHYS_M_ALL)
|
||||
expect(g.body != -1)
|
||||
expect(g.body != id)
|
||||
expect(Math.abs(g.y) < 0.01)
|
||||
|
|
@ -59,14 +74,13 @@ program PhysicsTest {
|
|||
let id = phys_static_add(physics_st, post, 2.0, 0.0, 2.0, 0.0)
|
||||
phys_settle(physics_st)
|
||||
let n = phys_count(physics_st)
|
||||
let near = phys_overlap(physics_st, 2.0, 1.0, 2.0, 0.5, PHYS_M_STATIC)
|
||||
expect_eq(len(near), 1)
|
||||
expect_eq(near[0], id)
|
||||
expect_eq(len(phys_overlap(physics_st, 2.0, 1.0, 2.0, 0.5, PHYS_M_GROUND)), 0)
|
||||
expect_eq(phys_overlap(physics_st, 2.0, 1.0, 2.0, 0.5, PHYS_M_STATIC), 1)
|
||||
expect_eq(phys_overlap_id(physics_st, 0), id)
|
||||
expect_eq(phys_overlap(physics_st, 2.0, 1.0, 2.0, 0.5, PHYS_M_GROUND), 0)
|
||||
phys_remove(physics_st, id)
|
||||
expect_eq(phys_count(physics_st), n - 1)
|
||||
expect_eq(len(phys_overlap(physics_st, 2.0, 1.0, 2.0, 0.5, PHYS_M_STATIC)), 0)
|
||||
expect(phys_pose(physics_st, id) == null)
|
||||
expect_eq(phys_overlap(physics_st, 2.0, 1.0, 2.0, 0.5, PHYS_M_STATIC), 0)
|
||||
expect(pose(physics_st, id) == null)
|
||||
phys_close(physics_st)
|
||||
}
|
||||
|
||||
|
|
@ -86,11 +100,11 @@ program PhysicsTest {
|
|||
let post = phys_offset(physics_st, phys_cylinder(physics_st, 1.0, 0.3), 0.0, 1.0, 0.0, 0.0)
|
||||
phys_static_add(physics_st, post, 0.0, 0.0, 0.0, 0.0)
|
||||
phys_settle(physics_st)
|
||||
let p = phys_push(physics_st, 0.5, 0.0, 0.4, 0.1, 1.8, PHYS_M_STATIC)
|
||||
let p = push_of(physics_st, 0.5, 0.0, 0.4, 0.1, 1.8, PHYS_M_STATIC)
|
||||
expect_eq(p.n, 1)
|
||||
expect(p.x > 0.1)
|
||||
expect(Math.abs(p.z) < 0.05)
|
||||
let clear = phys_push(physics_st, 3.0, 0.0, 0.4, 0.1, 1.8, PHYS_M_STATIC)
|
||||
let clear = push_of(physics_st, 3.0, 0.0, 0.4, 0.1, 1.8, PHYS_M_STATIC)
|
||||
expect_eq(clear.n, 0)
|
||||
phys_close(physics_st)
|
||||
}
|
||||
|
|
@ -141,8 +155,8 @@ program PhysicsTest {
|
|||
expect(g >= 0)
|
||||
phys_ground_add(physics_st, g)
|
||||
phys_settle(physics_st)
|
||||
let odd = phys_ray(physics_st, 5.5, 50.0, 10.5, 0.0, -100.0, 0.0, PHYS_M_GROUND)
|
||||
let even = phys_ray(physics_st, 6.5, 50.0, 10.5, 0.0, -100.0, 0.0, PHYS_M_GROUND)
|
||||
let odd = ray(physics_st, 5.5, 50.0, 10.5, 0.0, -100.0, 0.0, PHYS_M_GROUND)
|
||||
let even = ray(physics_st, 6.5, 50.0, 10.5, 0.0, -100.0, 0.0, PHYS_M_GROUND)
|
||||
expect(Math.abs(odd.y - 2.0) < 0.01)
|
||||
expect(Math.abs(even.y - 1.0) < 0.01)
|
||||
phys_close(physics_st)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,16 @@ import "ludic.physics"
|
|||
import "ludic.base"
|
||||
program WalkerTest {
|
||||
numbers float
|
||||
function pose(physics_st: mut PhysicsState, id: int) -> PhysPose {
|
||||
let p = new PhysPose
|
||||
if not phys_pose(physics_st, id, p) { return null }
|
||||
return p
|
||||
}
|
||||
function wpose(physics_st: mut PhysicsState, id: int) -> PhysWalk {
|
||||
let p = new PhysWalk
|
||||
if not phys_walker_pose(physics_st, id, p) { return null }
|
||||
return p
|
||||
}
|
||||
|
||||
function flat(physics_st: mut PhysicsState) -> void {
|
||||
expect(phys_open(physics_st, 1024, 1))
|
||||
|
|
@ -20,11 +30,11 @@ program WalkerTest {
|
|||
flat(physics_st)
|
||||
let me = phys_walker_add(physics_st, 0.35, 1.8, 0.0, 1.0, 0.0, 50.0, 70.0)
|
||||
walk(physics_st, me, 1.0, 0.0, 0.0)
|
||||
let p = phys_walker_pose(physics_st, me)
|
||||
let p = wpose(physics_st, me)
|
||||
expect_eq(p.ground, PHYS_ON_GROUND)
|
||||
expect(Math.abs(p.y) < 0.05)
|
||||
walk(physics_st, me, 2.0, 3.0, 0.0)
|
||||
let q = phys_walker_pose(physics_st, me)
|
||||
let q = wpose(physics_st, me)
|
||||
expect(Math.abs(q.x - 6.0) < 0.2)
|
||||
expect(q.body >= 0)
|
||||
phys_close(physics_st)
|
||||
|
|
@ -40,12 +50,12 @@ program WalkerTest {
|
|||
let a = phys_walker_add(physics_st, 0.35, 1.8, 0.0, 0.0, 0.0, 50.0, 70.0)
|
||||
phys_walker_limits(physics_st, a, 0.55, 0.5, 50.0)
|
||||
walk(physics_st, a, 1.5, 2.0, 0.0)
|
||||
let pa = phys_walker_pose(physics_st, a)
|
||||
let pa = wpose(physics_st, a)
|
||||
expect(pa.x > 2.5)
|
||||
expect(Math.abs(pa.y - 0.3) < 0.05)
|
||||
let b = phys_walker_add(physics_st, 0.35, 1.8, 0.0, 0.0, 10.0, 50.0, 70.0)
|
||||
walk(physics_st, b, 2.0, 2.0, 0.0)
|
||||
let pb = phys_walker_pose(physics_st, b)
|
||||
let pb = wpose(physics_st, b)
|
||||
expect(pb.x < 1.7)
|
||||
expect(Math.abs(pb.y) < 0.05)
|
||||
phys_close(physics_st)
|
||||
|
|
@ -57,11 +67,11 @@ program WalkerTest {
|
|||
walk(physics_st, me, 0.5, 0.0, 0.0)
|
||||
phys_walker_step(physics_st, me, 1.0 / 60.0, 0.0, 0.0, 5.0)
|
||||
walk(physics_st, me, 0.25, 0.0, 0.0)
|
||||
let up = phys_walker_pose(physics_st, me)
|
||||
let up = wpose(physics_st, me)
|
||||
expect_eq(up.ground, PHYS_IN_AIR)
|
||||
expect(up.y > 0.5)
|
||||
walk(physics_st, me, 1.5, 0.0, 0.0)
|
||||
let down = phys_walker_pose(physics_st, me)
|
||||
let down = wpose(physics_st, me)
|
||||
expect_eq(down.ground, PHYS_ON_GROUND)
|
||||
expect(Math.abs(down.y) < 0.05)
|
||||
phys_close(physics_st)
|
||||
|
|
@ -75,12 +85,12 @@ program WalkerTest {
|
|||
phys_walker_step(physics_st, me, 1.0 / 60.0, 2.0, 0.0, 0.0)
|
||||
phys_step(physics_st)
|
||||
}
|
||||
expect(phys_pose(physics_st, crate).x > 2.5)
|
||||
expect(pose(physics_st, crate).x > 2.5)
|
||||
phys_walker_place(physics_st, me, -10.0, 0.0, -10.0)
|
||||
let p = phys_walker_pose(physics_st, me)
|
||||
let p = wpose(physics_st, me)
|
||||
expect(Math.abs(p.x + 10.0) < 0.01)
|
||||
phys_walker_remove(physics_st, me)
|
||||
expect(phys_walker_pose(physics_st, me) == null)
|
||||
expect(wpose(physics_st, me) == null)
|
||||
phys_close(physics_st)
|
||||
}
|
||||
|
||||
|
|
@ -94,11 +104,11 @@ program WalkerTest {
|
|||
phys_step(physics_st)
|
||||
}
|
||||
expect(phys_spin_y(physics_st, hull) > 0.1)
|
||||
expect(phys_pose(physics_st, hull).z > 0.5)
|
||||
let p = phys_pose(physics_st, hull)
|
||||
expect(pose(physics_st, hull).z > 0.5)
|
||||
let p = pose(physics_st, hull)
|
||||
p.x = 5.0
|
||||
phys_set_pose(physics_st, hull, p)
|
||||
expect(Math.abs(phys_pose(physics_st, hull).x - 5.0) < 0.01)
|
||||
expect(Math.abs(pose(physics_st, hull).x - 5.0) < 0.01)
|
||||
phys_close(physics_st)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,19 +53,18 @@ export function phys_walker_step(physics_st: mut PhysicsState, id: int, dt: floa
|
|||
return jph_char_step(physics_st.ph_world, k, dt, vx, vz, jump)
|
||||
}
|
||||
|
||||
export function phys_walker_pose(physics_st: mut PhysicsState, id: int) -> PhysWalk {
|
||||
# where a walker is, into the caller's record; false when there is no such walker
|
||||
export function phys_walker_pose(physics_st: mut PhysicsState, id: int, p: PhysWalk) -> bool {
|
||||
let k = ph_walker(physics_st, id)
|
||||
if k == null { return null }
|
||||
ph_buffers()
|
||||
if k == null { return false }
|
||||
jph_char_read(k, physics_st.ph_vals)
|
||||
let v = physics_st.ph_vals
|
||||
let p = new PhysWalk
|
||||
p.x = v[0]; p.y = v[1]; p.z = v[2]
|
||||
p.vx = v[3]; p.vy = v[4]; p.vz = v[5]
|
||||
p.ground = int(v[6])
|
||||
p.ny = v[7]
|
||||
p.body = int(v[8])
|
||||
return p
|
||||
return true
|
||||
}
|
||||
|
||||
# put it somewhere, standing still: a teleport, a swap, getting off a boat
|
||||
|
|
@ -77,11 +76,12 @@ export function phys_walker_place(physics_st: mut PhysicsState, id: int, x: floa
|
|||
# a body the game keeps elsewhere, moved by its walker: put where the body is if something else
|
||||
# moved it (a teleport, a ride, a swim), then one step under the given limits; the ground after it
|
||||
export function phys_walker_move(physics_st: mut PhysicsState, id: int, x: float, y: float, z: float, vx: float, vz: float, jump: float, step: float, max_slope: float, dt: float) -> int {
|
||||
let p = phys_walker_pose(physics_st, id)
|
||||
if p == null { return PHYS_IN_AIR }
|
||||
let dx = p.x - x
|
||||
let dy = p.y - y
|
||||
let dz = p.z - z
|
||||
let k = ph_walker(physics_st, id)
|
||||
if k == null { return PHYS_IN_AIR }
|
||||
jph_char_read(k, physics_st.ph_vals)
|
||||
let dx = physics_st.ph_vals[0] - x
|
||||
let dy = physics_st.ph_vals[1] - y
|
||||
let dz = physics_st.ph_vals[2] - z
|
||||
if dx * dx + dy * dy + dz * dz > 0.0025 { phys_walker_place(physics_st, id, x, y, z) }
|
||||
phys_walker_limits(physics_st, id, step, 0.5, max_slope)
|
||||
return phys_walker_step(physics_st, id, dt, vx, vz, jump)
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ export state Render3dState {
|
|||
gltf_comps: int = 0
|
||||
gltf_tex_paths: []pointer = null
|
||||
gltf_tex_ids: words = null
|
||||
gltf_tex_refs: words = null # how many primitives use each cached texture (model_release)
|
||||
gltf_tex_n: int = 0
|
||||
gltf_white: int = 0
|
||||
gltf_flat: int = 0
|
||||
|
|
|
|||
|
|
@ -50,17 +50,17 @@ function gltf_mat_remember(render3d_st: mut Render3dState, name: string, diff: i
|
|||
}
|
||||
|
||||
function gltf_texture(render3d_st: mut Render3dState, uri: string, srgb: bool) -> int {
|
||||
if render3d_st.gltf_tex_paths == null { render3d_st.gltf_tex_paths = new []pointer; render3d_st.gltf_tex_ids = words(256) }
|
||||
if render3d_st.gltf_tex_paths == null { render3d_st.gltf_tex_paths = new []pointer; render3d_st.gltf_tex_ids = words(256); render3d_st.gltf_tex_refs = words(256) }
|
||||
var png: string = uri
|
||||
let n = len(uri)
|
||||
if n > 4 and uri[n - 4] == '.' and uri[n - 3] == 'j' { png = uri[0 .. n - 4] + ".png" }
|
||||
let path = render3d_st.gltf_dir + "/" + png
|
||||
var i = 0
|
||||
while i < render3d_st.gltf_tex_n { if render3d_st.gltf_tex_paths[i] == path { return render3d_st.gltf_tex_ids[i] }; i += 1 }
|
||||
while i < render3d_st.gltf_tex_n { if render3d_st.gltf_tex_paths[i] == path { render3d_st.gltf_tex_refs[i] += 1; return render3d_st.gltf_tex_ids[i] }; i += 1 }
|
||||
var dil = 0
|
||||
if render3d_st.gltf_cutout { dil = 24 }
|
||||
let id = tex_load_ex(render3d_st, path, srgb, dil)
|
||||
if render3d_st.gltf_tex_n < 256 { push(render3d_st.gltf_tex_paths, path); render3d_st.gltf_tex_ids[render3d_st.gltf_tex_n] = id; render3d_st.gltf_tex_n += 1 }
|
||||
if render3d_st.gltf_tex_n < 256 { push(render3d_st.gltf_tex_paths, path); render3d_st.gltf_tex_ids[render3d_st.gltf_tex_n] = id; render3d_st.gltf_tex_refs[render3d_st.gltf_tex_n] = 1; render3d_st.gltf_tex_n += 1 }
|
||||
return id
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +150,10 @@ function gltf_prim(render3d_st: mut Render3dState, p: Val) -> Prim {
|
|||
if ud != null { gltf_mat_remember(render3d_st, mname, pr.diff, pr.nrm, pr.arm) }
|
||||
else {
|
||||
let k = gltf_mat_find(render3d_st, mname)
|
||||
if k >= 0 { pr.diff = render3d_st.gltf_mat_diff[k]; pr.nrm = render3d_st.gltf_mat_nrm[k]; pr.arm = render3d_st.gltf_mat_arm[k] }
|
||||
if k >= 0 {
|
||||
pr.diff = render3d_st.gltf_mat_diff[k]; pr.nrm = render3d_st.gltf_mat_nrm[k]; pr.arm = render3d_st.gltf_mat_arm[k]
|
||||
gltf_tex_ref(render3d_st, pr.diff); gltf_tex_ref(render3d_st, pr.nrm); gltf_tex_ref(render3d_st, pr.arm)
|
||||
}
|
||||
else { print(`gltf: material {mname} has no textures and none were loaded before it`) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
70
packages/ludic.render3d/gltf_release.ludic
Normal file
70
packages/ludic.render3d/gltf_release.ludic
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# gltf_release.ludic - a loaded model let go of: its meshes, and each texture once nothing else uses
|
||||
# it. The glTF loader shares textures between models (a path loaded once, a LOD chain borrowing its
|
||||
# LOD0's material), so each cached texture carries a count of the primitives using it.
|
||||
|
||||
# one more primitive uses the cached texture `id` (0, or a texture the cache did not load: nothing)
|
||||
function gltf_tex_ref(render3d_st: mut Render3dState, id: int) -> void {
|
||||
if id == 0 or render3d_st.gltf_tex_refs == null { return }
|
||||
for i in 0 .. render3d_st.gltf_tex_n { if render3d_st.gltf_tex_ids[i] == id { render3d_st.gltf_tex_refs[i] += 1; return } }
|
||||
}
|
||||
|
||||
# one primitive fewer: the last one frees the texture and forgets it, with every material naming it
|
||||
function gltf_tex_unref(render3d_st: mut Render3dState, id: int) -> void {
|
||||
if id == 0 or render3d_st.gltf_tex_refs == null { return }
|
||||
for i in 0 .. render3d_st.gltf_tex_n {
|
||||
if render3d_st.gltf_tex_ids[i] == id {
|
||||
render3d_st.gltf_tex_refs[i] -= 1
|
||||
if render3d_st.gltf_tex_refs[i] > 0 { return }
|
||||
gpu_tex_free(render3d_st, id)
|
||||
gltf_tex_forget(render3d_st, i)
|
||||
gltf_mats_forget(render3d_st, id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# the cache entry i goes; the last entry takes its place
|
||||
function gltf_tex_forget(render3d_st: mut Render3dState, i: int) -> void {
|
||||
let last = render3d_st.gltf_tex_n - 1
|
||||
let paths = render3d_st.gltf_tex_paths
|
||||
paths[i] = paths[last]
|
||||
render3d_st.gltf_tex_ids[i] = render3d_st.gltf_tex_ids[last]
|
||||
render3d_st.gltf_tex_refs[i] = render3d_st.gltf_tex_refs[last]
|
||||
List.pop(paths)
|
||||
render3d_st.gltf_tex_n = last
|
||||
}
|
||||
|
||||
# a remembered material whose texture was freed is forgotten, so no later load borrows it
|
||||
function gltf_mats_forget(render3d_st: mut Render3dState, id: int) -> void {
|
||||
if render3d_st.gltf_mat_names == null { return }
|
||||
var k = 0
|
||||
while k < render3d_st.gltf_mat_n {
|
||||
if render3d_st.gltf_mat_diff[k] == id or render3d_st.gltf_mat_nrm[k] == id or render3d_st.gltf_mat_arm[k] == id {
|
||||
let last = render3d_st.gltf_mat_n - 1
|
||||
let names = render3d_st.gltf_mat_names
|
||||
names[k] = names[last]
|
||||
render3d_st.gltf_mat_diff[k] = render3d_st.gltf_mat_diff[last]
|
||||
render3d_st.gltf_mat_nrm[k] = render3d_st.gltf_mat_nrm[last]
|
||||
render3d_st.gltf_mat_arm[k] = render3d_st.gltf_mat_arm[last]
|
||||
List.pop(names)
|
||||
render3d_st.gltf_mat_n = last
|
||||
} else {
|
||||
k += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Let a model go: every primitive's mesh, and its textures once no other model uses them. The
|
||||
# caller says when - no actor may draw the model afterwards (a released model's prims are emptied,
|
||||
# so a stale draw of it draws nothing).
|
||||
function model_release(render3d_st: mut Render3dState, m: Model) -> void {
|
||||
if m == null or m.prims == null { return }
|
||||
for i in 0 .. len(m.prims) {
|
||||
let pr = m.prims[i]
|
||||
if pr.mesh != null { gpu_mesh_free(render3d_st, pr.mesh); pr.mesh = null }
|
||||
gltf_tex_unref(render3d_st, pr.diff); gltf_tex_unref(render3d_st, pr.nrm); gltf_tex_unref(render3d_st, pr.arm)
|
||||
pr.diff = 0; pr.nrm = 0; pr.arm = 0
|
||||
}
|
||||
let prims = m.prims
|
||||
while len(prims) > 0 { List.pop(prims) }
|
||||
}
|
||||
|
|
@ -1903,8 +1903,9 @@ function gvk_pipeline_fast(render3d_st: mut Render3dState, p: int, m: Mesh, st:
|
|||
return render3d_st.gvk_pc_pipe[k]
|
||||
}
|
||||
}
|
||||
# a pipeline the driver refused is remembered as 0 too: retried on every draw, each try built its
|
||||
# key and its create structs again and gave none of them back
|
||||
let pipe = gvk_pipeline(render3d_st, p, m, st, n_color, color_fmt, depth_fmt, samples)
|
||||
if pipe == 0 { return pipe }
|
||||
push(render3d_st.gvk_pc_prog, p); push(render3d_st.gvk_pc_layout, layout); push(render3d_st.gvk_pc_state, state)
|
||||
push(render3d_st.gvk_pc_bias, bias); push(render3d_st.gvk_pc_pass, pass); push(render3d_st.gvk_pc_pipe, pipe)
|
||||
if p < 4096 { render3d_st.gvk_pc_last[p] = len(render3d_st.gvk_pc_prog) - 1 }
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@ function gvk_tex_read(render3d_st: mut Render3dState, tex: int, ifmt: int, w: in
|
|||
let depth = gvk_is_depth(ifmt)
|
||||
let cb = gvk_once_begin(render3d_st)
|
||||
gvk_barrier(render3d_st, cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
|
||||
let bic = bytes(VkBufferImageCopy_sizeof)
|
||||
let bic = gvk_tmp(render3d_st, VkBufferImageCopy_sizeof)
|
||||
Vk.zero(bic, VkBufferImageCopy_sizeof)
|
||||
if depth { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) }
|
||||
else { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) }
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import "shadow.ludic"
|
|||
import "post.ludic"
|
||||
import "quat.ludic"
|
||||
import "gltf.ludic"
|
||||
import "gltf_release.ludic"
|
||||
import "skin.ludic"
|
||||
import "scatter.ludic"
|
||||
import "actor.ludic"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ property Layer {
|
|||
inst: floats, # INST_FLOATS per instance
|
||||
count: int = 0,
|
||||
cap: int = 0,
|
||||
have: int = 0, # instances inst and scratch have room for now: grown toward cap as filled
|
||||
near: float = 0.0, # float bits; instances beyond it draw as impostors (or not at all)
|
||||
cull: float = 0.0, # float bits; instances beyond it are skipped (0 = never)
|
||||
buf: int = 0,
|
||||
|
|
@ -386,8 +387,11 @@ function layer_new(render3d_st: mut Render3dState, model: Model, cap: int, folia
|
|||
l.near = near
|
||||
l.cull = cull
|
||||
l.tint = v3_new(1.0, 1.0, 1.0)
|
||||
l.inst = floats(cap * INST_FLOATS)
|
||||
l.scratch = floats(cap * INST_FLOATS)
|
||||
# room for what the layer holds, not for what it might: 45 layers at full capacity up front were
|
||||
# 0.4 GB, most of it never filled (plan 23 of maroon-lake)
|
||||
l.have = min(cap, 256)
|
||||
l.inst = floats(l.have * INST_FLOATS)
|
||||
l.scratch = floats(l.have * INST_FLOATS)
|
||||
l.last_cam = v3_new(100000.0, 0.0, 0.0)
|
||||
l.buf = gpu_buffer_new(render3d_st)
|
||||
l.imp_buf = gpu_buffer_new(render3d_st)
|
||||
|
|
@ -398,8 +402,23 @@ function layer_new(render3d_st: mut Render3dState, model: Model, cap: int, folia
|
|||
return l
|
||||
}
|
||||
|
||||
# room for n instances (at most the layer's cap), doubling what there is so a fill costs a few copies
|
||||
function layer_room(l: Layer, n: int) -> void {
|
||||
if n <= l.have { return }
|
||||
var want = max(l.have * 2, n)
|
||||
if want > l.cap { want = l.cap }
|
||||
let inst = floats(want * INST_FLOATS)
|
||||
if l.count > 0 { mem_copy(data_of(inst), data_of(l.inst), l.count * INST_FLOATS * 4) }
|
||||
free(l.inst)
|
||||
free(l.scratch)
|
||||
l.inst = inst
|
||||
l.scratch = floats(want * INST_FLOATS)
|
||||
l.have = want
|
||||
}
|
||||
|
||||
function layer_add(l: Layer, x: float, y: float, z: float, scale: float, yaw: float, seed: float, wind: float) -> void {
|
||||
if l.count >= l.cap { return }
|
||||
layer_room(l, l.count + 1)
|
||||
let o = l.count * INST_FLOATS
|
||||
l.inst[o] = x; l.inst[o + 1] = y; l.inst[o + 2] = z; l.inst[o + 3] = scale
|
||||
l.inst[o + 4] = Math.sin(yaw); l.inst[o + 5] = Math.cos(yaw); l.inst[o + 6] = seed; l.inst[o + 7] = wind
|
||||
|
|
|
|||
|
|
@ -162,16 +162,24 @@ function stream_evict(render3d_st: mut Render3dState, s: Stream) -> void {
|
|||
}
|
||||
t = lo
|
||||
# everything wanted by the walk in progress stays whatever the threshold says
|
||||
let kept = new []Chunk
|
||||
# compacted in place, and an evicted chunk goes whole: a new list per eviction and the chunks'
|
||||
# own records were never given back
|
||||
var w = 0
|
||||
var i = 0
|
||||
while i < s.n {
|
||||
let c = s.chunks[i]
|
||||
if c.used >= t or c.used == render3d_st.stream_walk_no { push(kept, c) }
|
||||
else { if c.data != null { free(c.data) } }
|
||||
if c.used >= t or c.used == render3d_st.stream_walk_no {
|
||||
s.chunks[w] = c
|
||||
w += 1
|
||||
} else {
|
||||
if c.data != null { free(c.data) }
|
||||
free(c)
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
s.chunks = kept
|
||||
s.n = len(kept)
|
||||
let ch = s.chunks
|
||||
while len(ch) > w { List.pop(ch) }
|
||||
s.n = w
|
||||
for h in 0 .. STREAM_HASH { s.htab[h] = 0 }
|
||||
i = 0
|
||||
while i < s.n { s.keys[i] = s.chunks[i].key; stream_remember(s, s.chunks[i].key, i); i += 1 }
|
||||
|
|
@ -246,6 +254,7 @@ function stream_update(render3d_st: mut Render3dState, s: Stream, cam_x: float,
|
|||
}
|
||||
if c != null and c.count > 0 and l.count + c.count <= l.cap and stream_chunk_visible(render3d_st, s, cx, cz, c) {
|
||||
let tg = gl_now_us()
|
||||
layer_room(l, l.count + c.count)
|
||||
mem_copy(mem_off(l.inst, l.count * INST_FLOATS * 4), data_of(c.data), c.count * INST_FLOATS * 4)
|
||||
l.count += c.count
|
||||
render3d_st.stream_us_gather = render3d_st.stream_us_gather + (gl_now_us() - tg)
|
||||
|
|
|
|||
|
|
@ -145,6 +145,9 @@ function terrain_ortho_forest(render3d_st: mut Render3dState, x: float, z: float
|
|||
function terrain_use_ortho(render3d_st: mut Render3dState, path: string) -> void {
|
||||
let px = png_decode(render3d_st, path)
|
||||
if px == null { return }
|
||||
# a map set up over another lets go of the last one's photograph first
|
||||
if render3d_st.ter_ortho_px != null { free(render3d_st.ter_ortho_px) }
|
||||
if render3d_st.ter_ortho_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_ortho_tex) }
|
||||
render3d_st.ter_ortho_px = px
|
||||
render3d_st.ter_ortho_w = render3d_st.tex_w
|
||||
render3d_st.ter_ortho_c = render3d_st.tex_channels
|
||||
|
|
@ -154,6 +157,7 @@ function terrain_use_ortho(render3d_st: mut Render3dState, path: string) -> void
|
|||
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
|
||||
}
|
||||
function terrain_use_dem(render3d_st: mut Render3dState, path: string, emin: float, emax: float, base: float, ox: float, oz: float) -> void {
|
||||
if render3d_st.ter_dem_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_dem_tex) }
|
||||
render3d_st.ter_dem_tex = tex_load(render3d_st, path, false)
|
||||
render3d_st.ter_dem_min = emin; render3d_st.ter_dem_max = emax; render3d_st.ter_dem_base = base
|
||||
render3d_st.ter_ox = ox; render3d_st.ter_oz = oz
|
||||
|
|
@ -167,6 +171,8 @@ function terrain_generate(render3d_st: mut Render3dState) -> void {
|
|||
if render3d_st.ter_dem_tex != 0 { defs = "#define DEM\n" }
|
||||
if render3d_st.ter_smooth { defs = "#define SMOOTH\n" }
|
||||
let p = r3d_program(render3d_st, "fullscreen.vert", "heightgen.frag", defs)
|
||||
# a map generated over another: its 256 MB height texture goes first
|
||||
if render3d_st.ter_height_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_height_tex); render3d_st.ter_height_tex = 0 }
|
||||
render3d_st.ter_height_tex = tex_target(render3d_st, TERRAIN_RES, TERRAIN_RES, GL_R32F, GL_RED, GL_FLOAT, GL_LINEAR)
|
||||
let fbo = gpu_fb_new(render3d_st)
|
||||
gpu_fb_bind(render3d_st, fbo)
|
||||
|
|
@ -203,8 +209,9 @@ function terrain_generate(render3d_st: mut Render3dState) -> void {
|
|||
mesh_draw(render3d_st, render3d_st.sky_fullscreen)
|
||||
gpu_program_free(render3d_st, pn)
|
||||
gpu_tex_free(render3d_st, raw)
|
||||
# read the heights back for placement
|
||||
render3d_st.ter_heights = floats(TERRAIN_RES * TERRAIN_RES)
|
||||
# read the heights back for placement, into the array a previous map had: always the same size,
|
||||
# and made anew on every build it was 64 MB lost per world swap
|
||||
if render3d_st.ter_heights == null { render3d_st.ter_heights = floats(TERRAIN_RES * TERRAIN_RES) }
|
||||
gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.ter_height_tex)
|
||||
gpu_pixel_store(render3d_st, GL_PACK_ALIGNMENT, 4)
|
||||
gpu_tex_read(render3d_st, GPU_TEX2D, GL_RED, GL_FLOAT, data_of(render3d_st.ter_heights))
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ function an_apply(ui_st: mut UiState, n: UiNode) -> void {
|
|||
ui_err(ui_st, `animation {n.anim}: there is no @keyframes of that name`)
|
||||
return
|
||||
}
|
||||
var t = (an_now(ui_st) - an_began(ui_st, `{n.key}@{n.anim}`)) / n.anim_dur
|
||||
var t = (an_now(ui_st) - an_began(ui_st, mm_join(ui_st, n.key, "@", n.anim))) / n.anim_dur
|
||||
if n.anim_loop {
|
||||
let lap = int(t)
|
||||
t = t - float(lap)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
# attr.ludic - what a property sets, by its CSS name or the short one (width or w, background or
|
||||
# bg, font-size or size, ...). An attribute on an element is the same property, applied last.
|
||||
const AT_ALIAS: string = " width=w height=h min-width=min-w max-width=max-w min-height=min-h max-height=max-h padding=pad background=bg background-color=bg font-size=size opacity=alpha flex-grow=grow flex-shrink=shrink flex-direction=dir flex-wrap=wrap justify-content=justify align-items=align align-self=self row-gap=gap column-gap=gap text-align=talign "
|
||||
const AT_UNDRAWN: string = " font-weight font-family font-style box-shadow cursor transition animation letter-spacing text-decoration "
|
||||
# a stylesheet's property: one that means nothing is said
|
||||
function el_attr(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> void {
|
||||
|
|
@ -12,44 +11,57 @@ function el_apply(ui_st: mut UiState, n: UiNode, key: string, v0: Val) -> bool {
|
|||
let k = at_alias(key)
|
||||
ui_st.el_cur = n
|
||||
ui_st.el_key = k
|
||||
if len(k) > 2 and k[0..2] == "--" {
|
||||
cs_var_set(n, k, vl_text(v0))
|
||||
if len(k) > 2 and sat(k, 0, "--") {
|
||||
cs_var_set(n, k, vl_text_u(ui_st, v0))
|
||||
return true
|
||||
}
|
||||
let v = cs_vars_in(ui_st, n, v0)
|
||||
return el_size(ui_st, n, k, v) or el_box(ui_st, n, k, v) or el_flex(n, k, v) or el_look(ui_st, n, k, v) or el_place_prop(ui_st, n, k, v) or el_edge(ui_st, n, k, v) or el_more(ui_st, n, k, v)
|
||||
return el_size(ui_st, n, k, v) or el_box(ui_st, n, k, v) or el_flex(ui_st, n, k, v) or el_look(ui_st, n, k, v) or el_place_prop(ui_st, n, k, v) or el_edge(ui_st, n, k, v) or el_more(ui_st, n, k, v)
|
||||
}
|
||||
function at_alias(key: string) -> string {
|
||||
let pat = ` {key}=`
|
||||
let n = len(pat)
|
||||
for i in 0 .. len(AT_ALIAS) - n + 1 {
|
||||
if AT_ALIAS[i..i + n] == pat {
|
||||
var j = i + n
|
||||
while AT_ALIAS[j] != 32 { j += 1 }
|
||||
return AT_ALIAS[i + n..j]
|
||||
}
|
||||
}
|
||||
# the CSS name to the short one; literals, so asking allocates nothing (asked per rule per element)
|
||||
if key == "width" { return "w" }
|
||||
if key == "height" { return "h" }
|
||||
if key == "min-width" { return "min-w" }
|
||||
if key == "max-width" { return "max-w" }
|
||||
if key == "min-height" { return "min-h" }
|
||||
if key == "max-height" { return "max-h" }
|
||||
if key == "padding" { return "pad" }
|
||||
if key == "background" { return "bg" }
|
||||
if key == "background-color" { return "bg" }
|
||||
if key == "font-size" { return "size" }
|
||||
if key == "opacity" { return "alpha" }
|
||||
if key == "flex-grow" { return "grow" }
|
||||
if key == "flex-shrink" { return "shrink" }
|
||||
if key == "flex-direction" { return "dir" }
|
||||
if key == "flex-wrap" { return "wrap" }
|
||||
if key == "justify-content" { return "justify" }
|
||||
if key == "align-items" { return "align" }
|
||||
if key == "align-self" { return "self" }
|
||||
if key == "row-gap" { return "gap" }
|
||||
if key == "column-gap" { return "gap" }
|
||||
if key == "text-align" { return "talign" }
|
||||
return key
|
||||
}
|
||||
function el_size(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
|
||||
if key == "w" or key == "h" { el_dim(ui_st, n, key == "w", v) } else if key == "min-w" { n.min_w = el_len(ui_st, v) } else if key == "max-w" { n.max_w = el_len(ui_st, v) } else if key == "min-h" { n.min_h = el_len(ui_st, v) } else if key == "max-h" { n.max_h = el_len(ui_st, v) } else if key == "gap" { n.gap = el_len(ui_st, v) } else { return false }
|
||||
return true
|
||||
}
|
||||
function el_flex(n: UiNode, key: string, v: Val) -> bool {
|
||||
let s = vl_text(v)
|
||||
function el_flex(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
|
||||
let s = vl_text_u(ui_st, v)
|
||||
if key == "dir" {
|
||||
n.dir = UI_COL
|
||||
if s == "row" { n.dir = UI_ROW }
|
||||
} else if key == "grow" { n.grow = el_num(s) } else if key == "flex" { el_flex_short(n, s) } else if key == "wrap" { n.wrap = s == "wrap" or (vl_truthy(v) and s != "false" and s != "nowrap") } else if key == "align" { n.align = el_place(v) } else if key == "self" { n.self = el_place(v) } else if key == "justify" { n.justify = el_place(v) } else if key == "talign" { n.talign = el_place(v) } else if key == "display" { n.hidden = s == "none" } else { return false }
|
||||
} else if key == "grow" { n.grow = el_num(s) } else if key == "flex" { el_flex_short(ui_st, n, s) } else if key == "wrap" { n.wrap = s == "wrap" or (vl_truthy(v) and s != "false" and s != "nowrap") } else if key == "align" { n.align = el_place(v) } else if key == "self" { n.self = el_place(v) } else if key == "justify" { n.justify = el_place(v) } else if key == "talign" { n.talign = el_place(v) } else if key == "display" { n.hidden = s == "none" } else { return false }
|
||||
return true
|
||||
}
|
||||
function el_look(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
|
||||
let s = vl_text(v)
|
||||
let s = vl_text_u(ui_st, v)
|
||||
if key == "size" { n.size = el_len(ui_st, v) } else if key == "color" {
|
||||
n.fg = ui_color(ui_st, s)
|
||||
n.fg_a = ui_st.cl_alpha
|
||||
} else if key == "bg" {
|
||||
if gr_is(tpl_words(s)) { nx(n).grad = gr_parse(ui_st, s) } else if len(s) > 4 and s[0..4] == "url(" { n.bg_img = el_url(s) } else if tpl_words(s) == "transparent" {
|
||||
if gr_is(tpl_words(s)) { nx(n).grad = gr_parse(ui_st, s) } else if len(s) > 4 and sat(s, 0, "url(") { n.bg_img = el_url(s) } else if tpl_words(s) == "transparent" {
|
||||
n.bg = 0
|
||||
n.bg_a = 0.0
|
||||
} else {
|
||||
|
|
@ -64,7 +76,7 @@ function el_look(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
|
|||
}
|
||||
# border: 1 #ffcc00, or 1px solid accent
|
||||
function el_border(ui_st: mut UiState, n: UiNode, s: string) -> void {
|
||||
let parts = lss_split(s, 32)
|
||||
let parts = mm_split(ui_st, s, 32)
|
||||
for i in 0 .. len(parts) {
|
||||
let p = parts[i]
|
||||
if len(p) > 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 46) { el_sides_border(n, el_unit(ui_st, p)) } else if p == "none" { el_sides_border(n, 0.0) } else if len(p) > 0 and p != "solid" {
|
||||
|
|
|
|||
|
|
@ -18,25 +18,32 @@ function el_box(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
|
|||
}
|
||||
# top right bottom left out of 1 to 4 lengths
|
||||
function el_sides(ui_st: mut UiState, v: Val) -> void {
|
||||
let parts = new []float
|
||||
if vl_num(v) { push(parts, el_px(ui_st, Value.as_float(v))) } else {
|
||||
let words = el_words(vl_text(v))
|
||||
var n = 0
|
||||
if vl_num(v) {
|
||||
ui_st.el_s0 = el_px(ui_st, Value.as_float(v))
|
||||
n = 1
|
||||
} else {
|
||||
let words = mm_words(ui_st, vl_text_u(ui_st, v))
|
||||
for i in 0 .. len(words) {
|
||||
if len(words[i]) > 0 { push(parts, el_word_len(ui_st, words[i])) }
|
||||
if len(words[i]) > 0 and n < 4 {
|
||||
el_side_set(ui_st, n, el_word_len(ui_st, words[i]))
|
||||
n += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
while len(parts) < 4 {
|
||||
if len(parts) == 0 { push(parts, 0.0) } else if len(parts) == 1 { push(parts, parts[0]) } else if len(parts) == 2 { push(parts, parts[0]) } else { push(parts, parts[1]) }
|
||||
}
|
||||
ui_st.el_s0 = parts[0]
|
||||
ui_st.el_s1 = parts[1]
|
||||
ui_st.el_s2 = parts[2]
|
||||
ui_st.el_s3 = parts[3]
|
||||
if n == 0 { ui_st.el_s0 = 0.0 }
|
||||
if n < 2 { ui_st.el_s1 = ui_st.el_s0 }
|
||||
if n < 3 { ui_st.el_s2 = ui_st.el_s0 }
|
||||
if n < 4 { ui_st.el_s3 = ui_st.el_s1 }
|
||||
}
|
||||
# CSS's one to four sides, top right bottom left, without a list made for them
|
||||
function el_side_set(ui_st: mut UiState, i: int, v: float) -> void {
|
||||
if i == 0 { ui_st.el_s0 = v } else if i == 1 { ui_st.el_s1 = v } else if i == 2 { ui_st.el_s2 = v } else { ui_st.el_s3 = v }
|
||||
}
|
||||
# a length: a number, 10px, 1.5em, 2rem, 50vw, calc(...), fill, fit or auto (a percentage is el_pct's)
|
||||
function el_len(ui_st: mut UiState, v: Val) -> float {
|
||||
if vl_num(v) { return el_px(ui_st, Value.as_float(v)) }
|
||||
let s = vl_text(v)
|
||||
let s = vl_text_u(ui_st, v)
|
||||
if s == "fill" { return UI_FILL }
|
||||
if ca_is(s) { return el_calc_len(ui_st, s) }
|
||||
if s == "fit" or s == "auto" or (len(s) > 0 and s[len(s) - 1] == 37) { return 0.0 }
|
||||
|
|
@ -67,8 +74,8 @@ function el_num(s: string) -> float {
|
|||
# start, center, end, stretch; for justify also between, around, evenly - with CSS's spellings
|
||||
function el_place(v: Val) -> int {
|
||||
var s = vl_text(v)
|
||||
if len(s) > 6 and s[0..6] == "space-" { s = s[6..len(s)] }
|
||||
if len(s) > 5 and s[0..5] == "flex-" { s = s[5..len(s)] }
|
||||
if len(s) > 6 and sat(s, 0, "space-") { s = s[6..len(s)] }
|
||||
if len(s) > 5 and sat(s, 0, "flex-") { s = s[5..len(s)] }
|
||||
if s == "center" { return UI_CENTER }
|
||||
if s == "end" or s == "right" { return UI_END }
|
||||
if s == "stretch" or s == "between" { return 3 }
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
# attr_edge.ludic - border-radius, outline (and its width, colour and offset), scrollbar-color
|
||||
function el_edge(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
|
||||
let s = vl_text(v)
|
||||
let s = vl_text_u(ui_st, v)
|
||||
if key == "border-radius" { n.radius = el_len(ui_st, v) } else if key == "outline" { el_outline(ui_st, n, s) } else if key == "outline-width" { n.outline_w = el_len(ui_st, v) } else if key == "outline-offset" { n.outline_off = el_len(ui_st, v) } else if key == "outline-color" {
|
||||
n.outline_c = ui_color(ui_st, s)
|
||||
n.outline_a = ui_st.cl_alpha
|
||||
} else if key == "scrollbar-color" {
|
||||
let parts = lss_split(s, 32)
|
||||
let parts = mm_split(ui_st, s, 32)
|
||||
if len(parts) > 0 { n.bar_c = ui_color(ui_st, parts[0]) }
|
||||
if len(parts) > 1 { n.bar_track = ui_color(ui_st, parts[1]) }
|
||||
} else if key == "background-image" and gr_is(tpl_words(s)) { nx(n).grad = gr_parse(ui_st, s) } else if key == "background-image" { n.bg_img = el_url(s) } else if key == "border-image" { el_border_image(ui_st, n, s) } else if key == "box-shadow" { el_shadow(ui_st, n, s) } else if key == "animation" { el_animation(n, s) } else if key == "transition" { n.tr_dur = el_secs(s) } else { return false }
|
||||
} else if key == "background-image" and gr_is(tpl_words(s)) { nx(n).grad = gr_parse(ui_st, s) } else if key == "background-image" { n.bg_img = el_url(s) } else if key == "border-image" { el_border_image(ui_st, n, s) } else if key == "box-shadow" { el_shadow(ui_st, n, s) } else if key == "animation" { el_animation(ui_st, n, s) } else if key == "transition" { n.tr_dur = el_secs(s) } else { return false }
|
||||
return true
|
||||
}
|
||||
# url(path) or url("path") -> path
|
||||
function el_url(s: string) -> string {
|
||||
var t = tpl_words(s)
|
||||
if len(t) > 5 and t[0..4] == "url(" { t = tpl_words(t[4..len(t) - 1]) }
|
||||
if len(t) > 5 and sat(t, 0, "url(") { t = tpl_words(t[4..len(t) - 1]) }
|
||||
if len(t) >= 2 and (t[0] == 34 or t[0] == 39) { t = t[1..len(t) - 1] }
|
||||
if t == "none" { return "" }
|
||||
return t
|
||||
}
|
||||
# border-image: url(chip.png) 14 / 12px - the source's corner size, then how wide they are drawn
|
||||
function el_border_image(ui_st: UiState, n: UiNode, s: string) -> void {
|
||||
function el_border_image(ui_st: mut UiState, n: UiNode, s: string) -> void {
|
||||
var close = 0
|
||||
while close < len(s) and s[close] != 41 { close += 1 }
|
||||
n.bi_src = el_url(s[0..Math.min(close + 1, len(s))])
|
||||
let rest = lss_split(s[Math.min(close + 1, len(s))..len(s)], 47)
|
||||
let rest = mm_split(ui_st, s[Math.min(close + 1, len(s))..len(s)], 47)
|
||||
n.bi_slice = el_num(tpl_words(rest[0]))
|
||||
n.bi_width = el_px(ui_st, n.bi_slice)
|
||||
if len(rest) > 1 { n.bi_width = el_unit(ui_st, tpl_words(rest[1])) }
|
||||
|
|
@ -35,7 +35,7 @@ function el_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void {
|
|||
n.sh_c = -2
|
||||
return
|
||||
}
|
||||
let parts = lss_split(s, 32)
|
||||
let parts = mm_split(ui_st, s, 32)
|
||||
var nums = 0
|
||||
var col = ""
|
||||
for i in 0 .. len(parts) {
|
||||
|
|
@ -53,7 +53,7 @@ function el_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void {
|
|||
}
|
||||
# outline: 2px solid #ffcc00, or none
|
||||
function el_outline(ui_st: mut UiState, n: UiNode, s: string) -> void {
|
||||
let parts = lss_split(s, 32)
|
||||
let parts = mm_split(ui_st, s, 32)
|
||||
n.outline_w = 0.0
|
||||
for i in 0 .. len(parts) {
|
||||
let p = parts[i]
|
||||
|
|
@ -64,8 +64,8 @@ function el_outline(ui_st: mut UiState, n: UiNode, s: string) -> void {
|
|||
}
|
||||
}
|
||||
# animation: fade 0.25s infinite alternate
|
||||
function el_animation(n: UiNode, s: string) -> void {
|
||||
let parts = lss_split(s, 32)
|
||||
function el_animation(ui_st: mut UiState, n: UiNode, s: string) -> void {
|
||||
let parts = mm_split(ui_st, s, 32)
|
||||
n.anim = ""
|
||||
for i in 0 .. len(parts) {
|
||||
let p = parts[i]
|
||||
|
|
@ -78,7 +78,7 @@ function el_secs(s: string) -> float {
|
|||
for i in 0 .. len(parts) {
|
||||
let p = parts[i]
|
||||
if len(p) > 1 and p[0] >= 48 and p[0] <= 57 or len(p) > 1 and p[0] == 46 {
|
||||
if len(p) > 2 and p[len(p) - 2..len(p)] == "ms" { return el_num(p) / 1000.0 }
|
||||
if len(p) > 2 and sat(p, len(p) - 2, "ms") { return el_num(p) / 1000.0 }
|
||||
return el_num(p)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# for pictures and natives, zoom (zoom.ludic), and a list split at its top-level commas
|
||||
function el_fit_props(ui_st: mut UiState, n: UiNode, key: string, s: string) -> bool {
|
||||
if key == "aspect-ratio" {
|
||||
let parts = lss_split(s, 47)
|
||||
let parts = mm_split(ui_st, s, 47)
|
||||
var r = el_num(tpl_words(parts[0]))
|
||||
if len(parts) > 1 and el_num(tpl_words(parts[1])) > 0.0 { r = r / el_num(tpl_words(parts[1])) }
|
||||
if tpl_words(s) == "auto" { r = 0.0 }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# percentage is kept apart and taken of the containing block's width (left, right) or height (top,
|
||||
# bottom) when the element is placed
|
||||
function el_inset(ui_st: mut UiState, n: UiNode, side: int, v: Val) -> float {
|
||||
let s = vl_text(v)
|
||||
let s = vl_text_u(ui_st, v)
|
||||
var pct = 0.0
|
||||
var px = UI_AUTO
|
||||
if s != "auto" {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
# attr_more.ludic - width and height (where 0 is 0 and calc() may hold a percentage), the flex
|
||||
# shorthand, flex-shrink, order, text-shadow, pointer-events, text-fit and line-height (text_fit.ludic)
|
||||
function el_more(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
|
||||
let s = vl_text(v)
|
||||
let s = vl_text_u(ui_st, v)
|
||||
if key == "shrink" { nx(n).shrink = Math.max(el_num(s), 0.0) } else if key == "order" { nx(n).order = int(el_num(s)) } else if key == "text-shadow" { el_text_shadow(ui_st, n, s) } else if key == "pointer-events" { nx(n).no_pointer = tpl_words(s) == "none" } else if key == "text-fit" { el_text_fit(ui_st, n, s) } else if key == "line-height" { el_line_h(ui_st, n, s) } else { return el_fit_props(ui_st, n, key, s) }
|
||||
return true
|
||||
}
|
||||
# width or height: a length (0 included), fill, a percentage, calc(), or auto/fit (none)
|
||||
function el_dim(ui_st: mut UiState, n: UiNode, wide: bool, v: Val) -> void {
|
||||
let s = vl_text(v)
|
||||
let s = vl_text_u(ui_st, v)
|
||||
var fixed = UI_AUTO
|
||||
var pct = 0.0
|
||||
var off = 0.0
|
||||
|
|
@ -40,8 +40,8 @@ function el_calc_len(ui_st: mut UiState, s: string) -> float {
|
|||
return ui_st.ca_px
|
||||
}
|
||||
# flex: 1 | flex: 1 0 | flex: 2 1 auto | flex: none - the grow, then the shrink
|
||||
function el_flex_short(n: UiNode, s: string) -> void {
|
||||
let parts = el_words(s)
|
||||
function el_flex_short(ui_st: mut UiState, n: UiNode, s: string) -> void {
|
||||
let parts = mm_words(ui_st, s)
|
||||
if s == "none" {
|
||||
n.grow = 0.0
|
||||
nx(n).shrink = 0.0
|
||||
|
|
@ -72,7 +72,7 @@ function el_text_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void {
|
|||
x.tsh_set = true
|
||||
x.tsh_c = -2
|
||||
if tpl_words(s) == "none" { return }
|
||||
let parts = el_words(s)
|
||||
let parts = mm_words(ui_st, s)
|
||||
var nums = 0
|
||||
var col = ""
|
||||
for i in 0 .. len(parts) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# attr_place.ludic - position and its insets, z-index, overflow, white-space and text-overflow
|
||||
function el_place_prop(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
|
||||
let s = vl_text(v)
|
||||
let s = vl_text_u(ui_st, v)
|
||||
if key == "position" {
|
||||
n.pos = 0
|
||||
if s == "relative" { n.pos = 1 }
|
||||
|
|
|
|||
|
|
@ -32,6 +32,19 @@ function ui_default_backend() -> UiBackend {
|
|||
return UiRenderers[0].make()
|
||||
}
|
||||
export state UiState {
|
||||
pl_pools: []UiPool = new []UiPool # pool.ludic: each screen's nodes and envs, used again
|
||||
pl_cur: UiPool = null
|
||||
mm: UiMemo = null # memo.ludic: splits, gradients and keys by their text
|
||||
mm_ints: []string = null
|
||||
mm_ivals: []Val = null
|
||||
ev_depth: int = 0 # eval.ludic: a call's arguments, in lists kept one a depth
|
||||
ev_args: []Val = new []Val
|
||||
ly_depth: int = 0 # layout_run.ludic: the lists a line lays out with, one a depth
|
||||
ly_sizes: [][]float = new [][]float
|
||||
ly_frozen: [][]bool = new [][]bool
|
||||
cs_rules: []UiRule = new []UiRule # cascade_merge.ludic: a component root's rules, gathered
|
||||
cs_envs: []UiEnv = new []UiEnv
|
||||
cs_seen: []UiSheet = new []UiSheet
|
||||
an_frames: []UiKeyframes = new []UiKeyframes
|
||||
an_keys: []string = new []string
|
||||
an_start: []float = new []float
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function bd_kids(ui_st: mut UiState, kids: []UiTpl, e0: UiEnv, key: string, out:
|
|||
for i in 0 .. len(kids) {
|
||||
let k = kids[i]
|
||||
ui_st.ui_at = k.where
|
||||
let ck = `{key}/{i}`
|
||||
let ck = mm_join(ui_st, key, "/", mm_int(ui_st, i))
|
||||
if k.tag == "state" or k.tag == "style" { continue }
|
||||
if k.tag == "let" {
|
||||
e = bd_let(ui_st, k, e)
|
||||
|
|
@ -47,12 +47,12 @@ function bd_each(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiN
|
|||
let at_name = bd_name(k, "index", "index")
|
||||
let by = tpl_attr(k, "key")
|
||||
for j in 0 .. Value.count(list) {
|
||||
let le = env_new(e)
|
||||
let le = env_new(ui_st, e)
|
||||
env_bind(le, as_name, Value.at(list, j))
|
||||
env_bind(le, at_name, Value.int(j))
|
||||
var id = string(j)
|
||||
env_bind(le, at_name, mm_ival(ui_st, j))
|
||||
var id = mm_int(ui_st, j)
|
||||
if by != null { id = ui_eval_text(ui_st, by, le) }
|
||||
bd_kids(ui_st, k.kids, le, `{key}#{id}`, out)
|
||||
bd_kids(ui_st, k.kids, le, mm_join(ui_st, key, "#", id), out)
|
||||
}
|
||||
}
|
||||
# <slot/> is what the component's user put inside it; <slot name="x"/> their <template slot="x">
|
||||
|
|
@ -77,7 +77,7 @@ function bd_comp(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiN
|
|||
}
|
||||
if k.comp == null { return false }
|
||||
let c = k.comp
|
||||
let ce = env_new(null)
|
||||
let ce = env_new(ui_st, null)
|
||||
ce.model = e.model
|
||||
ce.ctx = e.ctx
|
||||
for i in 0 .. len(c.keys) {
|
||||
|
|
@ -86,7 +86,6 @@ function bd_comp(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiN
|
|||
for i in 0 .. len(k.keys) { env_bind(ce, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
|
||||
ce.on_names = k.on_keys
|
||||
ce.on_acts = k.on_acts
|
||||
ce.on_envs = new []UiEnv
|
||||
for i in 0 .. len(k.on_keys) { push(ce.on_envs, e) }
|
||||
ce.slot = k
|
||||
ce.slot_env = e
|
||||
|
|
|
|||
|
|
@ -5,13 +5,15 @@ function bd_class(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []Ui
|
|||
let cls = k.cls
|
||||
uc_load(ui_st, cls)
|
||||
let it = in_get(ui_st, key, cls)
|
||||
let props = Value.object()
|
||||
if it.props == null { it.props = Value.object() }
|
||||
let props = it.props
|
||||
Value.clear(props)
|
||||
for i in 0 .. len(k.keys) {
|
||||
let a = k.keys[i]
|
||||
if a != "class" and a != "style" and a != "id" { Value.put(props, a, ui_eval(ui_st, k.vals[i], e)) }
|
||||
}
|
||||
cls.props(it.ptr, props)
|
||||
let ce = env_new(null)
|
||||
let ce = env_new(ui_st, null)
|
||||
ce.model = cls.model(it.ptr)
|
||||
ce.inst = it
|
||||
ce.ctx = e.ctx
|
||||
|
|
@ -19,7 +21,6 @@ function bd_class(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []Ui
|
|||
ce.slot_env = e
|
||||
ce.on_names = k.on_keys
|
||||
ce.on_acts = k.on_acts
|
||||
ce.on_envs = new []UiEnv
|
||||
for i in 0 .. len(k.on_keys) { push(ce.on_envs, e) }
|
||||
lc_note(ui_st, key, k.on_keys, k.on_acts, e)
|
||||
let at = len(out)
|
||||
|
|
@ -33,7 +34,7 @@ function bd_class(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []Ui
|
|||
function bd_pass_down(ui_st: mut UiState, n: UiNode, k: UiTpl, e: UiEnv) -> void {
|
||||
let c = tpl_attr(k, "class")
|
||||
if c != null {
|
||||
let parts = lss_split(ui_eval_text(ui_st, c, e), 32)
|
||||
let parts = mm_split(ui_st, ui_eval_text(ui_st, c, e), 32)
|
||||
for i in 0 .. len(parts) {
|
||||
if len(parts[i]) > 0 { push(n.classes, parts[i]) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
# build_more.ludic - <let name="{value}"/> names a value for the siblings after it, and
|
||||
# <provide name="{value}"> gives it to everything inside, components included (React's context)
|
||||
function bd_let(ui_st: mut UiState, k: UiTpl, e: UiEnv) -> UiEnv {
|
||||
let le = env_new(e)
|
||||
let le = env_new(ui_st, e)
|
||||
for i in 0 .. len(k.keys) { env_bind(le, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
|
||||
return le
|
||||
}
|
||||
function bd_provide(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
|
||||
let cx = env_new(e.ctx)
|
||||
let cx = env_new(ui_st, e.ctx)
|
||||
for i in 0 .. len(k.keys) { env_bind(cx, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
|
||||
let pe = env_new(e)
|
||||
let pe = env_new(ui_st, e)
|
||||
pe.ctx = cx
|
||||
bd_kids(ui_st, k.kids, pe, key, out)
|
||||
}
|
||||
|
|
@ -18,12 +18,11 @@ function bd_group(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []Ui
|
|||
bd_kids(ui_st, k.kids, e, key, out)
|
||||
return
|
||||
}
|
||||
let t = new UiNode
|
||||
let t = nd_take(ui_st)
|
||||
t.kind = UI_TEXT
|
||||
t.key = key
|
||||
t.text = ui_tr(ui_st, ui_eval_text(ui_st, k.text, e))
|
||||
t.env = e
|
||||
t.children = new []UiNode
|
||||
push(out, t)
|
||||
}
|
||||
# text inside text - <p>Hi <b>there</b></p>, <h3><slot/></h3> - reads as one line of it
|
||||
|
|
@ -37,5 +36,5 @@ function el_inline(n: UiNode) -> void {
|
|||
s = s + c.text
|
||||
}
|
||||
n.text = s
|
||||
n.children = new []UiNode
|
||||
List.clear(n.children)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function ca_fn_at(s: string, at: int) -> string {
|
|||
if ca_word(s, at, "clamp(") { return "clamp" }
|
||||
return ""
|
||||
}
|
||||
function ca_word(s: string, at: int, w: string) -> bool { return at + len(w) <= len(s) and s[at..at + len(w)] == w }
|
||||
function ca_word(s: string, at: int, w: string) -> bool { return at + len(w) <= len(s) and sat(s, at, w) }
|
||||
function ca_fn(ui_st: mut UiState, f: string) -> CaVal {
|
||||
ui_st.ca_i = ui_st.ca_i + len(f) + 1
|
||||
let args = new []CaVal
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@ function cs_tree(ui_st: mut UiState, n: UiNode) -> void {
|
|||
}
|
||||
if n.tag != "" { cs_node(ui_st, n) }
|
||||
for i in 0 .. len(n.children) { cs_tree(ui_st, n.children[i]) }
|
||||
var shown = new []UiNode
|
||||
var kept = 0
|
||||
for i in 0 .. len(n.children) {
|
||||
if not n.children[i].hidden { push(shown, n.children[i]) }
|
||||
if not n.children[i].hidden {
|
||||
n.children[kept] = n.children[i]
|
||||
kept += 1
|
||||
}
|
||||
}
|
||||
n.children = shown
|
||||
while len(n.children) > kept { List.pop(n.children) }
|
||||
cs_order(n)
|
||||
}
|
||||
# twice: the font size (and zoom) first, from every rule, so an em anywhere reads the size the
|
||||
|
|
@ -51,10 +54,10 @@ function cs_rule(ui_st: mut UiState, n: UiNode, r: UiRule) -> void { cs_rule_in(
|
|||
# a rule's custom properties first, so its other declarations can read them; values read in `e`
|
||||
function cs_rule_in(ui_st: mut UiState, n: UiNode, r: UiRule, e: UiEnv) -> void {
|
||||
for j in 0 .. len(r.keys) {
|
||||
if len(r.keys[j]) > 2 and r.keys[j][0..2] == "--" { el_attr(ui_st, n, r.keys[j], ui_eval(ui_st, r.vals[j], e)) }
|
||||
if len(r.keys[j]) > 2 and sat(r.keys[j], 0, "--") { el_attr(ui_st, n, r.keys[j], ui_eval(ui_st, r.vals[j], e)) }
|
||||
}
|
||||
for j in 0 .. len(r.keys) {
|
||||
if not (len(r.keys[j]) > 2 and r.keys[j][0..2] == "--") and cs_pass_ok(ui_st, r.keys[j]) { el_attr(ui_st, n, r.keys[j], ui_eval(ui_st, r.vals[j], e)) }
|
||||
if not (len(r.keys[j]) > 2 and sat(r.keys[j], 0, "--")) and cs_pass_ok(ui_st, r.keys[j]) { el_attr(ui_st, n, r.keys[j], ui_eval(ui_st, r.vals[j], e)) }
|
||||
}
|
||||
}
|
||||
function ua_sheet(ui_st: mut UiState) -> UiSheet {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@
|
|||
# specificity as one sheet's are. On a tie the user's rule wins (the outermost user's last), as a
|
||||
# later rule does. Then `order`, which rearranges a flex box's children without touching the tree.
|
||||
function cs_merged(ui_st: mut UiState, n: UiNode) -> void {
|
||||
let rules = new []UiRule
|
||||
let envs = new []UiEnv
|
||||
let seen = new []UiSheet
|
||||
let rules = ui_st.cs_rules # kept and emptied: a merge runs once an element, never inside another
|
||||
let envs = ui_st.cs_envs
|
||||
let seen = ui_st.cs_seen
|
||||
List.clear(rules)
|
||||
List.clear(envs)
|
||||
List.clear(seen)
|
||||
if n.sheet != null { cs_gather(ui_st, n, n.sheet, n.env, rules, envs, seen) }
|
||||
for i in 0 .. len(n.ups) {
|
||||
if n.ups[i].sheet != null { cs_gather(ui_st, n, n.ups[i].sheet, n.ups[i].env, rules, envs, seen) }
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ function ui_color(ui_st: mut UiState, name0: string) -> int {
|
|||
let name = tpl_words(name0)
|
||||
ui_st.cl_alpha = 1.0
|
||||
if name == "none" or name == "" or name == "transparent" { return UI_NONE }
|
||||
if name[0] == 35 { return cl_hex(ui_st, name[1..len(name)]) }
|
||||
if len(name) > 4 and (name[0..4] == "rgb(" or name[0..5] == "rgba(") { return cl_rgb(ui_st, name) }
|
||||
let pat = ` {name}=`
|
||||
let n = len(pat)
|
||||
for i in 0 .. len(CL_NAMES) - n + 1 {
|
||||
if CL_NAMES[i..i + n] == pat { return cl_hex(ui_st, CL_NAMES[i + n..i + n + 6]) }
|
||||
if name[0] == 35 { return cl_hex_at(ui_st, name, 1, len(name) - 1) }
|
||||
if len(name) > 4 and (sat(name, 0, "rgb(") or sat(name, 0, "rgba(")) { return cl_rgb(ui_st, name) }
|
||||
let n = len(name)
|
||||
for i in 0 .. len(CL_NAMES) - n - 1 {
|
||||
if CL_NAMES[i] == 32 and CL_NAMES[i + n + 1] == 61 and sat(CL_NAMES, i + 1, name) { return cl_hex_at(ui_st, CL_NAMES, i + n + 2, 6) }
|
||||
}
|
||||
ui_err(ui_st, `{name}: not a colour - #rrggbb, rgb(), rgba(), a name, or var(--a-colour)`)
|
||||
return 15658734
|
||||
|
|
@ -20,23 +19,24 @@ function cl_digit(c: int) -> int {
|
|||
if c >= 65 { return c - 55 }
|
||||
return c - 48
|
||||
}
|
||||
function cl_hex(ui_st: mut UiState, h: string) -> int {
|
||||
function cl_hex(ui_st: mut UiState, h: string) -> int { return cl_hex_at(ui_st, h, 0, len(h)) }
|
||||
# the n hex digits of `s` from `at`, read where they stand
|
||||
function cl_hex_at(ui_st: mut UiState, s: string, at: int, n: int) -> int {
|
||||
var c = 0
|
||||
let n = len(h)
|
||||
if n == 3 or n == 4 {
|
||||
for i in 0 .. 3 { c = c * 256 + cl_digit(h[i]) * 17 }
|
||||
if n == 4 { ui_st.cl_alpha = float(cl_digit(h[3]) * 17) / 255.0 }
|
||||
for i in 0 .. 3 { c = c * 256 + cl_digit(s[at + i]) * 17 }
|
||||
if n == 4 { ui_st.cl_alpha = float(cl_digit(s[at + 3]) * 17) / 255.0 }
|
||||
return c
|
||||
}
|
||||
for i in 0 .. 3 { c = c * 256 + cl_digit(h[i * 2]) * 16 + cl_digit(h[i * 2 + 1]) }
|
||||
if n == 8 { ui_st.cl_alpha = float(cl_digit(h[6]) * 16 + cl_digit(h[7])) / 255.0 }
|
||||
for i in 0 .. 3 { c = c * 256 + cl_digit(s[at + i * 2]) * 16 + cl_digit(s[at + i * 2 + 1]) }
|
||||
if n == 8 { ui_st.cl_alpha = float(cl_digit(s[at + 6]) * 16 + cl_digit(s[at + 7])) / 255.0 }
|
||||
return c
|
||||
}
|
||||
# rgb(217, 165, 74) or rgba(0, 0, 0, 0.55)
|
||||
function cl_rgb(ui_st: mut UiState, s: string) -> int {
|
||||
var open = 0
|
||||
while open < len(s) and s[open] != 40 { open += 1 }
|
||||
let parts = lss_split(s[open + 1..len(s) - 1], 44)
|
||||
let parts = mm_split(ui_st, s[open + 1..len(s) - 1], 44)
|
||||
var c = 0
|
||||
for i in 0 .. Math.min(3, len(parts)) { c = c * 256 + Math.clamp(int(el_num(tpl_words(parts[i]))), 0, 255) }
|
||||
if len(parts) > 3 { ui_st.cl_alpha = el_num(tpl_words(parts[3])) }
|
||||
|
|
|
|||
|
|
@ -4,18 +4,13 @@
|
|||
# input[type=text|number|key] .ui-label .ui-field > .ui-value (+ .ui-caret while focused)
|
||||
# select .ui-label .ui-prev .ui-value .ui-next (its <option>s hidden)
|
||||
# with a note="...", the label is .ui-labels > .ui-label + .ui-note
|
||||
function ct_part(n: UiNode, cls: string, text: string) -> UiNode {
|
||||
let p = new UiNode
|
||||
p.key = `{n.key}/{cls}`
|
||||
function ct_part(ui_st: mut UiState, n: UiNode, cls: string, text: string) -> UiNode {
|
||||
let p = nd_take(ui_st)
|
||||
p.key = mm_join(ui_st, n.key, "/", cls)
|
||||
p.tag = "span"
|
||||
p.env = n.env
|
||||
p.sheet = n.sheet
|
||||
p.children = new []UiNode
|
||||
p.classes = new []string
|
||||
push(p.classes, cls)
|
||||
p.akeys = new []string
|
||||
p.avals = new []string
|
||||
p.vals = new []Val
|
||||
if text != "" {
|
||||
p.kind = UI_TEXT
|
||||
p.text = text
|
||||
|
|
@ -23,18 +18,18 @@ function ct_part(n: UiNode, cls: string, text: string) -> UiNode {
|
|||
return p
|
||||
}
|
||||
# <progress value max> and <meter value min max>: a track with a fill as far as the value
|
||||
function ct_build_bar(n: UiNode) -> void {
|
||||
function ct_build_bar(ui_st: mut UiState, n: UiNode) -> void {
|
||||
n.dir = UI_ROW
|
||||
let lo = Value.as_float(ui_attr(n, "min"))
|
||||
var hi = Value.as_float(ui_attr(n, "max"))
|
||||
if hi <= lo { hi = lo + 1.0 }
|
||||
let fill = ct_part(n, "ui-fill", "")
|
||||
let fill = ct_part(ui_st, n, "ui-fill", "")
|
||||
fill.w_pct = Math.clamp((Value.as_float(ui_attr(n, "value")) - lo) / (hi - lo), 0.0, 1.0) * 100.0
|
||||
push(n.children, fill)
|
||||
}
|
||||
function ct_build(ui_st: UiState, n: UiNode) -> void {
|
||||
function ct_build(ui_st: mut UiState, n: UiNode) -> void {
|
||||
if n.tag == "progress" or n.tag == "meter" {
|
||||
ct_build_bar(n)
|
||||
ct_build_bar(ui_st, n)
|
||||
return
|
||||
}
|
||||
n.dir = UI_ROW
|
||||
|
|
@ -46,41 +41,41 @@ function ct_build(ui_st: UiState, n: UiNode) -> void {
|
|||
}
|
||||
ct_label(ui_st, n)
|
||||
if t == "checkbox" or t == "radio" {
|
||||
let track = ct_part(n, "ui-track", "")
|
||||
push(track.children, ct_part(n, "ui-knob", ""))
|
||||
let track = ct_part(ui_st, n, "ui-track", "")
|
||||
push(track.children, ct_part(ui_st, n, "ui-knob", ""))
|
||||
push(n.children, track)
|
||||
} else if t == "range" {
|
||||
ct_build_range(n)
|
||||
ct_build_range(ui_st, n)
|
||||
} else {
|
||||
let field = ct_part(n, "ui-field", "")
|
||||
push(field.children, ct_part(n, "ui-value", ct_field_text(ui_st, n)))
|
||||
if ui_st.fc_key == n.key and t != "key" { push(field.children, ct_part(n, "ui-caret", "")) }
|
||||
let field = ct_part(ui_st, n, "ui-field", "")
|
||||
push(field.children, ct_part(ui_st, n, "ui-value", ct_field_text(ui_st, n)))
|
||||
if ui_st.fc_key == n.key and t != "key" { push(field.children, ct_part(ui_st, n, "ui-caret", "")) }
|
||||
push(n.children, field)
|
||||
}
|
||||
}
|
||||
function ct_build_range(n: UiNode) -> void {
|
||||
function ct_build_range(ui_st: mut UiState, n: UiNode) -> void {
|
||||
let lo = Value.as_float(ui_attr(n, "min"))
|
||||
var hi = Value.as_float(ui_attr(n, "max"))
|
||||
if hi <= lo { hi = lo + 1.0 }
|
||||
let v = Value.as_float(ui_attr(n, "value"))
|
||||
let track = ct_part(n, "ui-track", "")
|
||||
let fill = ct_part(n, "ui-fill", "")
|
||||
let track = ct_part(ui_st, n, "ui-track", "")
|
||||
let fill = ct_part(ui_st, n, "ui-fill", "")
|
||||
fill.w_pct = Math.clamp((v - lo) / (hi - lo), 0.0, 1.0) * 100.0
|
||||
push(track.children, fill)
|
||||
push(track.children, ct_part(n, "ui-thumb", ""))
|
||||
push(track.children, ct_part(ui_st, n, "ui-thumb", ""))
|
||||
push(n.children, track)
|
||||
push(n.children, ct_part(n, "ui-value", ct_value_text(n)))
|
||||
push(n.children, ct_part(ui_st, n, "ui-value", ct_value_text(n)))
|
||||
}
|
||||
function ct_build_select(ui_st: UiState, n: UiNode) -> void {
|
||||
function ct_build_select(ui_st: mut UiState, n: UiNode) -> void {
|
||||
let opts = n.children
|
||||
n.children = new []UiNode
|
||||
ct_label(ui_st, n)
|
||||
var shown = ""
|
||||
let at = ct_option_at(n, opts)
|
||||
if at >= 0 { shown = opts[at].text }
|
||||
push(n.children, ct_part(n, "ui-prev", "<"))
|
||||
push(n.children, ct_part(n, "ui-value", shown))
|
||||
push(n.children, ct_part(n, "ui-next", ">"))
|
||||
push(n.children, ct_part(ui_st, n, "ui-prev", "<"))
|
||||
push(n.children, ct_part(ui_st, n, "ui-value", shown))
|
||||
push(n.children, ct_part(ui_st, n, "ui-next", ">"))
|
||||
n.opts = opts
|
||||
}
|
||||
# which option the select's value names: an option's value="...", else its place
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
# controls_parts.ludic - what a control shows besides its own parts: its label, with a `note` (a line
|
||||
# of help under it) in .ui-labels > .ui-label + .ui-note, and a range's or a number's value as text:
|
||||
# `decimals="1"` places, `format="percent"` its place between min and max, and a `unit` after it
|
||||
function ct_label(ui_st: UiState, n: UiNode) -> void {
|
||||
function ct_label(ui_st: mut UiState, n: UiNode) -> void {
|
||||
let label = ui_attr_text(n, "label")
|
||||
let note = ui_attr_text(n, "note")
|
||||
if note == "" {
|
||||
if label != "" { push(n.children, ct_part(n, "ui-label", ui_tr(ui_st, label))) }
|
||||
if label != "" { push(n.children, ct_part(ui_st, n, "ui-label", ui_tr(ui_st, label))) }
|
||||
return
|
||||
}
|
||||
let both = ct_part(n, "ui-labels", "")
|
||||
if label != "" { push(both.children, ct_part(n, "ui-label", ui_tr(ui_st, label))) }
|
||||
push(both.children, ct_part(n, "ui-note", ui_tr(ui_st, note)))
|
||||
let both = ct_part(ui_st, n, "ui-labels", "")
|
||||
if label != "" { push(both.children, ct_part(ui_st, n, "ui-label", ui_tr(ui_st, label))) }
|
||||
push(both.children, ct_part(ui_st, n, "ui-note", ui_tr(ui_st, note)))
|
||||
push(n.children, both)
|
||||
}
|
||||
function ct_value_text(n: UiNode) -> string {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,18 @@ export property UiGradient {
|
|||
alphas: []float = null
|
||||
at: []float = null # each stop's place, 0 to 1
|
||||
}
|
||||
function gr_is(s: string) -> bool { return len(s) > 16 and s[0..16] == "linear-gradient(" }
|
||||
function gr_is(s: string) -> bool { return len(s) > 16 and sat(s, 0, "linear-gradient(") }
|
||||
# a gradient is parsed once per text and shared by every element that wears it - never changed after
|
||||
function gr_parse(ui_st: mut UiState, s0: string) -> UiGradient {
|
||||
let m = mm_of(ui_st)
|
||||
let i = mm_find(m, MM_GRAD, s0)
|
||||
if i >= 0 { return m.grads[i] }
|
||||
let g = gr_parse_new(ui_st, s0)
|
||||
let j = mm_add(m, MM_GRAD, s0)
|
||||
if j >= 0 { m.grads[j] = g }
|
||||
return g
|
||||
}
|
||||
function gr_parse_new(ui_st: mut UiState, s0: string) -> UiGradient {
|
||||
let s = tpl_words(s0)
|
||||
let parts = el_commas(s[16..len(s) - 1])
|
||||
let g = new UiGradient
|
||||
|
|
@ -18,7 +28,7 @@ function gr_parse(ui_st: mut UiState, s0: string) -> UiGradient {
|
|||
var from = 0
|
||||
if len(parts) > 0 and gr_dir(g, tpl_words(parts[0])) { from = 1 }
|
||||
for i in from .. len(parts) {
|
||||
let words = el_words(tpl_words(parts[i]))
|
||||
let words = mm_words(ui_st, tpl_words(parts[i]))
|
||||
var col = tpl_words(parts[i])
|
||||
var at = -1.0
|
||||
let last = words[len(words) - 1]
|
||||
|
|
@ -36,7 +46,7 @@ function gr_parse(ui_st: mut UiState, s0: string) -> UiGradient {
|
|||
return g
|
||||
}
|
||||
function gr_dir(g: UiGradient, w: string) -> bool {
|
||||
if w == "to bottom" { g.dir = 0 } else if w == "to top" { g.dir = 1 } else if w == "to right" { g.dir = 2 } else if w == "to left" { g.dir = 3 } else if len(w) > 3 and w[len(w) - 3..len(w)] == "deg" {
|
||||
if w == "to bottom" { g.dir = 0 } else if w == "to top" { g.dir = 1 } else if w == "to right" { g.dir = 2 } else if w == "to left" { g.dir = 3 } else if len(w) > 3 and sat(w, len(w) - 3, "deg") {
|
||||
let d = int(Math.round(el_num(w) / 90.0)) % 4
|
||||
g.dir = 1
|
||||
if d == 1 or d == -3 { g.dir = 2 }
|
||||
|
|
|
|||
|
|
@ -4,19 +4,14 @@ const EL_BOXES: string = " col div progress meter section header footer nav main
|
|||
const EL_TEXTS: string = " text span p label h1 h2 h3 h4 h5 h6 strong em small b i a option "
|
||||
function bd_elem(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string) -> UiNode {
|
||||
ui_st.ui_at = k.where
|
||||
let n = new UiNode
|
||||
let n = nd_take(ui_st)
|
||||
n.key = key
|
||||
n.tag = k.tag
|
||||
n.env = e
|
||||
n.sheet = k.sheet
|
||||
n.style = k.style
|
||||
n.children = new []UiNode
|
||||
n.classes = new []string
|
||||
n.akeys = new []string
|
||||
n.avals = new []string
|
||||
n.vals = new []Val
|
||||
el_kind(ui_st, n, k.tag)
|
||||
for i in 0 .. len(k.keys) { el_keep(n, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
|
||||
for i in 0 .. len(k.keys) { el_keep(ui_st, n, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
|
||||
n.hovered = fr_was_hovered(ui_st, key)
|
||||
n.on_names = k.on_keys
|
||||
n.on_acts = k.on_acts
|
||||
|
|
@ -28,7 +23,7 @@ function bd_elem(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string) -> UiNode
|
|||
let say = ui_attr_text(n, "translate")
|
||||
if say == "no" { ui_st.tr_no = true } else if say == "yes" { ui_st.tr_no = false }
|
||||
bd_kids(ui_st, k.kids, e, key, n.children)
|
||||
if k.text != null { el_text(n, ui_tr(ui_st, ui_eval_text(ui_st, k.text, e))) }
|
||||
if k.text != null { el_text(ui_st, n, ui_tr(ui_st, ui_eval_text(ui_st, k.text, e))) }
|
||||
el_inline(n)
|
||||
if k.tag == "input" or k.tag == "select" or k.tag == "progress" or k.tag == "meter" { ct_build(ui_st, n) }
|
||||
ui_st.tr_no = was_no
|
||||
|
|
@ -45,23 +40,22 @@ function el_kind(ui_st: mut UiState, n: UiNode, tag: string) -> void {
|
|||
} else if tag == "input" or tag == "select" { n.focusable = true } else if tag == "spacer" { n.kind = UI_SPACER } else if tag == "scroll" { n.kind = UI_SCROLL } else if tag == "img" { n.kind = UI_IMAGE } else if tag == "hr" { n.kind = UI_RULE } else if not lk_in(EL_BOXES, tag) { ui_err(ui_st, `<{tag}>: there is no such element`) }
|
||||
}
|
||||
# a box with words in it holds them as a text of its own, as <div>words</div> does
|
||||
function el_text(n: UiNode, s: string) -> void {
|
||||
function el_text(ui_st: mut UiState, n: UiNode, s: string) -> void {
|
||||
if n.kind != UI_BOX and n.kind != UI_SCROLL {
|
||||
n.text = s
|
||||
return
|
||||
}
|
||||
let t = new UiNode
|
||||
let t = nd_take(ui_st)
|
||||
t.kind = UI_TEXT
|
||||
t.text = s
|
||||
t.env = n.env
|
||||
t.children = new []UiNode
|
||||
push(n.children, t)
|
||||
}
|
||||
# id, class, hidden, disabled and src say what the element is; the rest are applied after the rules
|
||||
function el_keep(n: UiNode, key: string, v: Val) -> void {
|
||||
let s = vl_text(v)
|
||||
function el_keep(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> void {
|
||||
let s = vl_text_u(ui_st, v)
|
||||
if key == "id" { n.id = s } else if key == "class" {
|
||||
let parts = lss_split(s, 32)
|
||||
let parts = mm_split(ui_st, s, 32)
|
||||
for i in 0 .. len(parts) {
|
||||
if len(parts[i]) > 0 { push(n.classes, parts[i]) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,8 @@ export property UiEnv {
|
|||
inst: UiInst = null # the Ludic component whose template this is
|
||||
}
|
||||
|
||||
function env_new(parent: UiEnv) -> UiEnv {
|
||||
let e = new UiEnv
|
||||
e.names = new []string
|
||||
e.vals = new []Val
|
||||
function env_new(ui_st: mut UiState, parent: UiEnv) -> UiEnv {
|
||||
let e = env_take(ui_st)
|
||||
e.parent = parent
|
||||
if parent != null {
|
||||
e.model = parent.model
|
||||
|
|
@ -84,7 +82,7 @@ export function ui_err(ui_st: mut UiState, msg0: string) -> void {
|
|||
function ui_has(s: string, part: string) -> bool {
|
||||
let n = len(part)
|
||||
for i in 0 .. len(s) - n + 1 {
|
||||
if s[i..i + n] == part { return true }
|
||||
if sat(s, i, part) { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,8 +34,18 @@ function ui_eval(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> Val {
|
|||
}
|
||||
# a call: len() and range() are the template's own, anything else is the view's
|
||||
function ev_call(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> Val {
|
||||
let args = Value.list()
|
||||
let depth = ui_st.ev_depth
|
||||
while len(ui_st.ev_args) <= depth { push(ui_st.ev_args, Value.list()) }
|
||||
let args = ui_st.ev_args[depth]
|
||||
Value.clear(args)
|
||||
ui_st.ev_depth = depth + 1
|
||||
for i in 0 .. len(x.args) { Value.add(args, ui_eval(ui_st, x.args[i], e)) }
|
||||
let r = ev_call_with(ui_st, x, e, args)
|
||||
ui_st.ev_depth = depth
|
||||
return r
|
||||
}
|
||||
# the call itself, its arguments evaluated; a callee reads them and keeps none
|
||||
function ev_call_with(ui_st: mut UiState, x: UiExpr, e: UiEnv, args: Val) -> Val {
|
||||
if x.s == "len" and len(x.args) == 1 { return Value.int(Value.count(Value.at(args, 0))) }
|
||||
if x.s == "range" and len(x.args) == 1 {
|
||||
let out = Value.list()
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ function rd_op(r: UiRd, op: string) -> bool {
|
|||
rd_ws(r)
|
||||
let n = len(op)
|
||||
if r.i + n > len(r.s) { return false }
|
||||
if r.s[r.i..r.i + n] != op { return false }
|
||||
if not sat(r.s, r.i, op) { return false }
|
||||
r.i += n
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,23 +39,19 @@ export function ui_nodes(ui_st: mut UiState, screen: string, view: UiView) -> Ui
|
|||
return null
|
||||
}
|
||||
let t = ui_st.lib_screens[si]
|
||||
pl_begin(ui_st, screen)
|
||||
lc_begin(ui_st)
|
||||
ui_st.ui_view = view
|
||||
let e = env_new(null)
|
||||
let e = env_new(ui_st, null)
|
||||
if view != null and view.model != null { e.model = view.model() } else { e.model = Value.object() }
|
||||
e.state = st_for(ui_st, screen, t, e)
|
||||
let root = new UiNode
|
||||
let root = nd_take(ui_st)
|
||||
root.key = screen
|
||||
root.children = new []UiNode
|
||||
root.env = e
|
||||
root.sheet = t.sheet
|
||||
root.classes = new []string
|
||||
root.akeys = new []string
|
||||
root.avals = new []string
|
||||
root.vals = new []Val
|
||||
root.tag = "screen"
|
||||
for i in 0 .. len(t.keys) {
|
||||
if t.keys[i] != "name" { el_keep(root, t.keys[i], ui_eval(ui_st, t.vals[i], e)) }
|
||||
if t.keys[i] != "name" { el_keep(ui_st, root, t.keys[i], ui_eval(ui_st, t.vals[i], e)) }
|
||||
}
|
||||
bd_kids(ui_st, t.kids, e, screen, root.children)
|
||||
cs_tree(ui_st, root)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ import "node.ludic"
|
|||
import "node_more.ludic"
|
||||
import "view.ludic"
|
||||
import "backend.ludic"
|
||||
import "pool.ludic"
|
||||
import "pool_reset.ludic"
|
||||
import "pool_reset_more.ludic"
|
||||
import "memo.ludic"
|
||||
import "memo_join.ludic"
|
||||
import "expr.ludic"
|
||||
import "expr_parse.ludic"
|
||||
import "expr_prim.ludic"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ property UiInst {
|
|||
cls: UiClass = null
|
||||
ptr: pointer = null
|
||||
seen: int = 0
|
||||
props: Val = null # its props, filled again each frame into the same object
|
||||
}
|
||||
function in_get(ui_st: mut UiState, key: string, k: UiClass) -> UiInst {
|
||||
for i in 0 .. len(ui_st.in_list) {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@ function ly_place_line(ui_st: mut UiState, n: UiNode, from: int, to: int, top: f
|
|||
var used = 0.0
|
||||
var grows = 0.0
|
||||
var count = 0
|
||||
let sizes = new []float
|
||||
let depth = ui_st.ly_depth
|
||||
ui_st.ly_depth += 1
|
||||
let sizes = ly_scratch(ui_st, depth)
|
||||
for i in from .. to {
|
||||
push(sizes, ly_basis(n.children[i], n.dir, main_room))
|
||||
if n.children[i].pos < 2 {
|
||||
|
|
@ -52,7 +54,7 @@ function ly_place_line(ui_st: mut UiState, n: UiNode, from: int, to: int, top: f
|
|||
}
|
||||
used = used + n.gap * float(Math.max(count - 1, 0))
|
||||
let free = main_room - used
|
||||
if free < 0.0 { ly_shrink_line(n, from, sizes, 0.0 - free) }
|
||||
if free < 0.0 { ly_shrink_line(ui_st, depth, n, from, sizes, 0.0 - free) }
|
||||
ly_justify(ui_st, n, free, count, grows)
|
||||
let extra = ui_st.ly_extra
|
||||
var at = ui_st.ly_lead
|
||||
|
|
@ -65,6 +67,17 @@ function ly_place_line(ui_st: mut UiState, n: UiNode, from: int, to: int, top: f
|
|||
if c.pos == 1 { ly_nudge(n, c) }
|
||||
at = at + m + n.gap + extra
|
||||
}
|
||||
ui_st.ly_depth = depth
|
||||
}
|
||||
# a line's sizes, and its shrink's frozen flags, in lists kept one a depth (a line's children lay out
|
||||
# their own lines while it is still placing them), emptied rather than made anew
|
||||
function ly_scratch(ui_st: mut UiState, depth: int) -> []float {
|
||||
while len(ui_st.ly_sizes) <= depth {
|
||||
push(ui_st.ly_sizes, new []float)
|
||||
push(ui_st.ly_frozen, new []bool)
|
||||
}
|
||||
List.clear(ui_st.ly_sizes[depth])
|
||||
return ui_st.ly_sizes[depth]
|
||||
}
|
||||
# one child: `m` along the run and its own size (or the whole room, stretched) across, less margins
|
||||
function ly_place_child(ui_st: mut UiState, n: UiNode, c: UiNode, at: float, top: float, m: float, cross_room: float) -> void {
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@ function ly_min_h(c: UiNode) -> float {
|
|||
return c.mh
|
||||
}
|
||||
# `sizes` (the children from..to) shrunk by `over` in all
|
||||
function ly_shrink_line(n: UiNode, from: int, sizes: []float, over0: float) -> void {
|
||||
function ly_shrink_line(ui_st: mut UiState, depth: int, n: UiNode, from: int, sizes: []float, over0: float) -> void {
|
||||
var over = over0
|
||||
let frozen = new []bool
|
||||
let frozen = ui_st.ly_frozen[depth]
|
||||
List.clear(frozen)
|
||||
for k in 0 .. len(sizes) { push(frozen, n.children[from + k].pos >= 2) }
|
||||
for pass in 0 .. 8 {
|
||||
var wsum = 0.0
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# layout_text.ludic - a text's size: one line, or as many as it takes to fit its room (white-space
|
||||
# normal), breaking between words; nowrap keeps one line, and text-overflow: ellipsis cuts it
|
||||
function ly_measure_text(ui_st: UiState, n: UiNode, avail: float) -> void {
|
||||
function ly_measure_text(ui_st: mut UiState, n: UiNode, avail: float) -> void {
|
||||
n.lines = null
|
||||
let lh = ly_line_h(n)
|
||||
var one = ly_text_w(ui_st, n.text, n.rsize)
|
||||
|
|
@ -19,8 +19,8 @@ function ly_measure_text(ui_st: UiState, n: UiNode, avail: float) -> void {
|
|||
n.mw = n.mw + widest
|
||||
n.mh = n.mh + lh * float(len(n.lines))
|
||||
}
|
||||
function ly_wrap(ui_st: UiState, s: string, size: float, room: float) -> []string {
|
||||
let words = lss_split(s, 32)
|
||||
function ly_wrap(ui_st: mut UiState, s: string, size: float, room: float) -> []string {
|
||||
let words = mm_split(ui_st, s, 32)
|
||||
let out = new []string
|
||||
var line = ""
|
||||
for i in 0 .. len(words) {
|
||||
|
|
|
|||
|
|
@ -39,3 +39,13 @@ function lib_beside(path: string, src: string) -> string {
|
|||
if cut < 0 { return src }
|
||||
return path[0..cut + 1] + src
|
||||
}
|
||||
# `s` has `p` starting at `at`: compared in place, where s[at..at + len(p)] == p made a new string
|
||||
# for every place it looked (Ludic frees nothing, and the cascade asks this per element per frame)
|
||||
function sat(s: string, at: int, p: string) -> bool {
|
||||
let n = len(p)
|
||||
if at < 0 or at + n > len(s) { return false }
|
||||
for i in 0 .. n {
|
||||
if s[at + i] != p[i] { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ function lc_note(ui_st: mut UiState, key: string, names: []string, acts: []UiAct
|
|||
}
|
||||
function lc_mine(key: string, screen: string) -> bool {
|
||||
let n = len(screen)
|
||||
return key == screen or (len(key) > n and key[0..n] == screen and key[n] == 47)
|
||||
return key == screen or (len(key) > n and sat(key, 0, screen) and key[n] == 47)
|
||||
}
|
||||
# the screen's mounts and unmounts since its last frame; other screens' records are kept
|
||||
function lc_run(ui_st: mut UiState, screen: string) -> void {
|
||||
|
|
|
|||
|
|
@ -20,10 +20,9 @@ function lk_kids(ui_st: mut UiState, f: UiFile, t: UiTpl) -> void {
|
|||
function lk_element(ui_st: UiState, tag: string) -> bool { return lk_in(LK_OWN, tag) or lk_in(EL_BOXES, tag) or lk_in(EL_TEXTS, tag) or nt_find(ui_st, tag) != null }
|
||||
# a word in a list of words written " a b c "
|
||||
function lk_in(words: string, w: string) -> bool {
|
||||
let pat = ` {w} `
|
||||
let n = len(pat)
|
||||
for i in 0 .. len(words) - n + 1 {
|
||||
if words[i..i + n] == pat { return true }
|
||||
let n = len(w)
|
||||
for i in 0 .. len(words) - n - 1 {
|
||||
if words[i] == 32 and words[i + n + 1] == 32 and sat(words, i + 1, w) { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
68
packages/ludic.ui/memo.ludic
Normal file
68
packages/ludic.ui/memo.ludic
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# memo.ludic - what a frame would work out again from the same text is kept by that text: a value
|
||||
# split into its words, a gradient, a key joined from its parent's. Ludic frees nothing, so parsing
|
||||
# "2px 6px" per element per frame was a leak; the table is bounded, and past it a result is made
|
||||
# (and lost) as before rather than the table growing without end.
|
||||
const MM_CAP: int = 8192 # slots; at most three quarters are used
|
||||
export property UiMemo {
|
||||
slots: words = null # index + 1 into keys, 0 empty
|
||||
keys: []string = null # the text a result was made from (or the joined text itself)
|
||||
tags: []int = null # what kind of result: a separator, or a kind below
|
||||
lists: [][]string = null
|
||||
grads: []UiGradient = null
|
||||
}
|
||||
const MM_WORDS: int = -1 # el_words: spaces, a bracket's own kept
|
||||
const MM_GRAD: int = -2
|
||||
const MM_JOIN: int = -3 # keys[i] is the joined text itself
|
||||
function mm_new() -> UiMemo {
|
||||
let m = new UiMemo
|
||||
m.slots = words(MM_CAP)
|
||||
m.keys = new []string
|
||||
m.tags = new []int
|
||||
m.lists = new [][]string
|
||||
m.grads = new []UiGradient
|
||||
return m
|
||||
}
|
||||
function mm_of(ui_st: mut UiState) -> UiMemo {
|
||||
if ui_st.mm == null { ui_st.mm = mm_new() }
|
||||
return ui_st.mm
|
||||
}
|
||||
# FNV-1a over the text, the tag mixed in first
|
||||
function mm_hash(tag: int, s: string) -> int {
|
||||
var h = 216613626 ^ (tag & 255)
|
||||
for i in 0 .. len(s) { h = ((h ^ s[i]) * 16777619) & 2147483647 }
|
||||
return h
|
||||
}
|
||||
# the entry for (tag, s), or -1
|
||||
function mm_find(m: UiMemo, tag: int, s: string) -> int {
|
||||
var at = mm_hash(tag, s) & (MM_CAP - 1)
|
||||
while m.slots[at] != 0 {
|
||||
let i = m.slots[at] - 1
|
||||
if m.tags[i] == tag and m.keys[i] == s { return i }
|
||||
at = (at + 1) & (MM_CAP - 1)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
# a new entry for (tag, s) with its results empty, or -1 when the table is full
|
||||
function mm_add(m: UiMemo, tag: int, s: string) -> int {
|
||||
if len(m.keys) * 4 >= MM_CAP * 3 { return -1 }
|
||||
var at = mm_hash(tag, s) & (MM_CAP - 1)
|
||||
while m.slots[at] != 0 { at = (at + 1) & (MM_CAP - 1) }
|
||||
push(m.keys, s)
|
||||
push(m.tags, tag)
|
||||
push(m.lists, null)
|
||||
push(m.grads, null)
|
||||
m.slots[at] = len(m.keys)
|
||||
return len(m.keys) - 1
|
||||
}
|
||||
# lss_split's parts of `s` at `c`, made once per text; the list is shared - read it, never change it
|
||||
function mm_split(ui_st: mut UiState, s: string, c: int) -> []string {
|
||||
let m = mm_of(ui_st)
|
||||
let i = mm_find(m, c, s)
|
||||
if i >= 0 { return m.lists[i] }
|
||||
var parts: []string = null
|
||||
if c == MM_WORDS { parts = el_words(s) } else { parts = lss_split(s, c) }
|
||||
let j = mm_add(m, c, s)
|
||||
if j >= 0 { m.lists[j] = parts }
|
||||
return parts
|
||||
}
|
||||
function mm_words(ui_st: mut UiState, s: string) -> []string { return mm_split(ui_st, s, MM_WORDS) }
|
||||
51
packages/ludic.ui/memo_join.ludic
Normal file
51
packages/ludic.ui/memo_join.ludic
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# memo_join.ludic - a key joined from its parent's key, a separator and a part ("App/3",
|
||||
# "App/3#7", "App/3/ui-thumb"), found by comparing in place, so an element's key is made once
|
||||
# the element first appears and never again
|
||||
function mm_hash_join(a: string, sep: string, b: string) -> int {
|
||||
var h = 216613626 ^ (MM_JOIN & 255)
|
||||
for i in 0 .. len(a) { h = ((h ^ a[i]) * 16777619) & 2147483647 }
|
||||
h = ((h ^ sep[0]) * 16777619) & 2147483647
|
||||
for i in 0 .. len(b) { h = ((h ^ b[i]) * 16777619) & 2147483647 }
|
||||
return h
|
||||
}
|
||||
function mm_is_join(k: string, a: string, sep: string, b: string) -> bool {
|
||||
let na = len(a)
|
||||
if len(k) != na + 1 + len(b) or k[na] != sep[0] { return false }
|
||||
return sat(k, 0, a) and sat(k, na + 1, b)
|
||||
}
|
||||
# a + sep + b, the same string every frame
|
||||
function mm_join(ui_st: mut UiState, a: string, sep: string, b: string) -> string {
|
||||
let m = mm_of(ui_st)
|
||||
var at = mm_hash_join(a, sep, b) & (MM_CAP - 1)
|
||||
while m.slots[at] != 0 {
|
||||
let i = m.slots[at] - 1
|
||||
if m.tags[i] == MM_JOIN and mm_is_join(m.keys[i], a, sep, b) { return m.keys[i] }
|
||||
at = (at + 1) & (MM_CAP - 1)
|
||||
}
|
||||
let k = a + sep + b
|
||||
if len(m.keys) * 4 >= MM_CAP * 3 { return k }
|
||||
push(m.keys, k)
|
||||
push(m.tags, MM_JOIN)
|
||||
push(m.lists, null)
|
||||
push(m.grads, null)
|
||||
m.slots[at] = len(m.keys)
|
||||
return k
|
||||
}
|
||||
# a small whole number's text, made once (a key's index, a loop's count)
|
||||
function mm_int(ui_st: mut UiState, i: int) -> string {
|
||||
if i < 0 or i >= 1024 { return string(i) }
|
||||
if ui_st.mm_ints == null {
|
||||
ui_st.mm_ints = new []string
|
||||
for k in 0 .. 1024 { push(ui_st.mm_ints, string(k)) }
|
||||
}
|
||||
return ui_st.mm_ints[i]
|
||||
}
|
||||
# a small whole number as a Value, made once and shared: read it, never change it (a loop's index)
|
||||
function mm_ival(ui_st: mut UiState, i: int) -> Val {
|
||||
if i < 0 or i >= 1024 { return Value.int(i) }
|
||||
if ui_st.mm_ivals == null {
|
||||
ui_st.mm_ivals = new []Val
|
||||
for k in 0 .. 1024 { push(ui_st.mm_ivals, Value.int(k)) }
|
||||
}
|
||||
return ui_st.mm_ivals[i]
|
||||
}
|
||||
|
|
@ -64,7 +64,7 @@ function nt_run(ui_st: mut UiState, n: UiNode, name: string, value: Val) -> void
|
|||
if (lk_in(PE_NAMES, name) or name == "hold") and Value.kind(value) == 6 { ev = value }
|
||||
Value.put(ev, "name", Value.str(name))
|
||||
Value.put(ev, "value", value)
|
||||
let e = env_new(n.env)
|
||||
let e = env_new(ui_st, n.env)
|
||||
env_bind(e, "event", ev)
|
||||
act_run(ui_st, n.on_acts[i], e)
|
||||
}
|
||||
|
|
@ -78,7 +78,13 @@ export function ui_attr(n: UiNode, key: string) -> Val {
|
|||
}
|
||||
return Value.null()
|
||||
}
|
||||
export function ui_attr_text(n: UiNode, key: string) -> string { return vl_text(ui_attr(n, key)) }
|
||||
export function ui_attr_text(n: UiNode, key: string) -> string {
|
||||
if n.akeys == null { return "" }
|
||||
for i in 0 .. len(n.akeys) {
|
||||
if n.akeys[i] == key { return vl_text(n.vals[i]) }
|
||||
}
|
||||
return ""
|
||||
}
|
||||
# a boolean attribute: present with no value is true, as HTML's are (<button autofocus>)
|
||||
export function ui_attr_on(n: UiNode, key: string) -> bool {
|
||||
if n.akeys == null { return false }
|
||||
|
|
|
|||
84
packages/ludic.ui/pool.ludic
Normal file
84
packages/ludic.ui/pool.ludic
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# pool.ludic - what a frame is built of is kept and used again: ludic.ui builds a screen's tree anew
|
||||
# every frame, and Ludic frees nothing, so a node or an env made per element per frame was a leak
|
||||
# (the valley's HUD: ~100 MB/s). Each screen keeps two generations and alternates, so last frame's
|
||||
# tree - which an unmount's env, a hover or ui_box still reads - stays whole while this one is built.
|
||||
# So a host may read a tree until the same screen has been built twice more, and never keep it longer.
|
||||
export property UiPool {
|
||||
key: string = ""
|
||||
gen: int = 0
|
||||
nodes0: []UiNode = null
|
||||
nodes1: []UiNode = null
|
||||
envs0: []UiEnv = null
|
||||
envs1: []UiEnv = null
|
||||
un: int = 0
|
||||
ue: int = 0
|
||||
}
|
||||
# a build of `screen` begins: its other generation is handed out again from the start
|
||||
function pl_begin(ui_st: mut UiState, screen: string) -> void {
|
||||
var p: UiPool = null
|
||||
for i in 0 .. len(ui_st.pl_pools) {
|
||||
if ui_st.pl_pools[i].key == screen { p = ui_st.pl_pools[i] }
|
||||
}
|
||||
if p == null {
|
||||
p = new UiPool
|
||||
p.key = screen
|
||||
p.nodes0 = new []UiNode
|
||||
p.nodes1 = new []UiNode
|
||||
p.envs0 = new []UiEnv
|
||||
p.envs1 = new []UiEnv
|
||||
push(ui_st.pl_pools, p)
|
||||
}
|
||||
p.gen = 1 - p.gen
|
||||
p.un = 0
|
||||
p.ue = 0
|
||||
ui_st.pl_cur = p
|
||||
}
|
||||
# a node, blank, with its own lists (empty) - from the pool when a build is under way
|
||||
function nd_take(ui_st: mut UiState) -> UiNode {
|
||||
let p = ui_st.pl_cur
|
||||
if p == null { return nd_fresh() }
|
||||
var list = p.nodes0
|
||||
if p.gen == 1 { list = p.nodes1 }
|
||||
if p.un < len(list) {
|
||||
let n = list[p.un]
|
||||
p.un += 1
|
||||
nd_reset(n)
|
||||
return n
|
||||
}
|
||||
let n = nd_fresh()
|
||||
push(list, n)
|
||||
p.un += 1
|
||||
return n
|
||||
}
|
||||
function nd_fresh() -> UiNode {
|
||||
let n = new UiNode
|
||||
n.children = new []UiNode
|
||||
n.classes = new []string
|
||||
n.akeys = new []string
|
||||
n.avals = new []string
|
||||
n.vals = new []Val
|
||||
return n
|
||||
}
|
||||
function env_take(ui_st: mut UiState) -> UiEnv {
|
||||
let p = ui_st.pl_cur
|
||||
if p == null { return env_fresh() }
|
||||
var list = p.envs0
|
||||
if p.gen == 1 { list = p.envs1 }
|
||||
if p.ue < len(list) {
|
||||
let e = list[p.ue]
|
||||
p.ue += 1
|
||||
env_reset(e)
|
||||
return e
|
||||
}
|
||||
let e = env_fresh()
|
||||
push(list, e)
|
||||
p.ue += 1
|
||||
return e
|
||||
}
|
||||
function env_fresh() -> UiEnv {
|
||||
let e = new UiEnv
|
||||
e.names = new []string
|
||||
e.vals = new []Val
|
||||
e.on_envs = new []UiEnv
|
||||
return e
|
||||
}
|
||||
61
packages/ludic.ui/pool_reset.ludic
Normal file
61
packages/ludic.ui/pool_reset.ludic
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# pool_reset.ludic - a pooled node put back to its defaults: every field, and the lists it owns
|
||||
# emptied and kept (generated from node.ludic - a field added there is added here)
|
||||
function nd_reset(n: UiNode) -> void {
|
||||
nd_reset_a(n)
|
||||
nd_reset_b(n)
|
||||
}
|
||||
function nd_reset_a(n: UiNode) -> void {
|
||||
n.kind = 0
|
||||
n.dir = 0
|
||||
n.text = ""
|
||||
n.key = ""
|
||||
n.tag = ""
|
||||
n.id = ""
|
||||
if n.classes != null { List.clear(n.classes) }
|
||||
if n.akeys != null { List.clear(n.akeys) }
|
||||
if n.avals != null { List.clear(n.avals) }
|
||||
if n.vals != null { List.clear(n.vals) }
|
||||
n.style = null
|
||||
n.sheet = null
|
||||
if n.ups != null { List.clear(n.ups) }
|
||||
n.parent = null
|
||||
n.index = 0
|
||||
n.src = ""
|
||||
n.size = 0.0
|
||||
n.w = -1000000.0
|
||||
n.h = -1000000.0
|
||||
n.w_pct = 0.0
|
||||
n.h_pct = 0.0
|
||||
n.pt = 0.0
|
||||
n.pr = 0.0
|
||||
n.pb = 0.0
|
||||
n.pl = 0.0
|
||||
n.mt = 0.0
|
||||
n.mr = 0.0
|
||||
n.mb = 0.0
|
||||
n.ml = 0.0
|
||||
n.border = 0.0
|
||||
n.bt = 0.0
|
||||
n.br = 0.0
|
||||
n.bb = 0.0
|
||||
n.bl = 0.0
|
||||
n.border_c = -2
|
||||
n.gap = 0.0
|
||||
n.align = 0
|
||||
n.justify = 0
|
||||
n.talign = 0
|
||||
n.self = -1
|
||||
n.grow = 0.0
|
||||
n.wrap = false
|
||||
n.min_w = 0.0
|
||||
n.max_w = 0.0
|
||||
n.min_h = 0.0
|
||||
n.max_h = 0.0
|
||||
n.hidden = false
|
||||
n.pos = 0
|
||||
n.top = -1000000.0
|
||||
n.right = -1000000.0
|
||||
n.bottom = -1000000.0
|
||||
n.left = -1000000.0
|
||||
n.z = 0
|
||||
}
|
||||
96
packages/ludic.ui/pool_reset_more.ludic
Normal file
96
packages/ludic.ui/pool_reset_more.ludic
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# pool_reset_more.ludic - the rest of a pooled node's fields, its extras and an env, to their defaults
|
||||
function nd_reset_b(n: UiNode) -> void {
|
||||
n.nowrap = false
|
||||
n.ellipsis = false
|
||||
if n.lines != null { List.clear(n.lines) }
|
||||
if n.var_names != null { List.clear(n.var_names) }
|
||||
if n.var_vals != null { List.clear(n.var_vals) }
|
||||
n.fg = -1
|
||||
n.fg_a = 1.0
|
||||
n.bg = -2
|
||||
n.bg_a = 1.0
|
||||
n.border_a = 1.0
|
||||
n.rfg_a = 1.0
|
||||
n.alpha = 1.0
|
||||
n.enabled = true
|
||||
n.act = null
|
||||
n.on_names = null
|
||||
n.on_acts = null
|
||||
n.native = null
|
||||
n.env = null
|
||||
if n.children != null { List.clear(n.children) }
|
||||
n.mw = 0.0
|
||||
n.mh = 0.0
|
||||
n.content_h = 0.0
|
||||
n.x = 0.0
|
||||
n.y = 0.0
|
||||
n.cw = 0.0
|
||||
n.ch = 0.0
|
||||
n.rsize = 0.0
|
||||
n.rfg = 0
|
||||
n.hovered = false
|
||||
n.focusable = false
|
||||
n.radius = 0.0
|
||||
n.outline_w = 0.0
|
||||
n.outline_c = -1
|
||||
n.outline_a = 1.0
|
||||
n.outline_off = 0.0
|
||||
n.scroll_y = 0.0
|
||||
n.bar_c = -1
|
||||
n.bar_track = -2
|
||||
n.opts = null
|
||||
n.bg_img = ""
|
||||
n.bi_src = ""
|
||||
n.bi_slice = 0.0
|
||||
n.bi_width = 0.0
|
||||
n.sh_x = 0.0
|
||||
n.sh_y = 0.0
|
||||
n.sh_c = -2
|
||||
n.sh_a = 1.0
|
||||
n.anim = ""
|
||||
n.anim_dur = 0.0
|
||||
n.anim_loop = false
|
||||
n.anim_alt = false
|
||||
n.tr_dur = 0.0
|
||||
n.popover = false
|
||||
if n.ex != null { nx_reset(n.ex) }
|
||||
}
|
||||
function nx_reset(x: UiNodeX) -> void {
|
||||
x.shrink = -1.0
|
||||
x.order = 0
|
||||
x.w_off = 0.0
|
||||
x.h_off = 0.0
|
||||
x.mh_min = 0.0
|
||||
x.has_min = false
|
||||
x.tsh_set = false
|
||||
x.tsh_x = 0.0
|
||||
x.tsh_y = 0.0
|
||||
x.tsh_c = -2
|
||||
x.tsh_a = 1.0
|
||||
x.no_pointer = false
|
||||
x.fit_min = 0.0
|
||||
x.lh_kind = 0
|
||||
x.lh_v = 0.0
|
||||
x.ip_t = 0.0
|
||||
x.ip_r = 0.0
|
||||
x.ip_b = 0.0
|
||||
x.ip_l = 0.0
|
||||
x.grad = null
|
||||
x.ratio = 0.0
|
||||
x.fit_obj = 0
|
||||
x.zoom = 1.0
|
||||
}
|
||||
function env_reset(e: UiEnv) -> void {
|
||||
if e.names != null { List.clear(e.names) }
|
||||
if e.vals != null { List.clear(e.vals) }
|
||||
e.state = null
|
||||
e.parent = null
|
||||
e.model = null
|
||||
e.slot = null
|
||||
e.slot_env = null
|
||||
e.on_names = null
|
||||
e.on_acts = null
|
||||
if e.on_envs != null { List.clear(e.on_envs) }
|
||||
e.ctx = null
|
||||
e.inst = null
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ function r3u_image_h(render3d_st: mut Render3dState, ui_st: mut UiState, src: st
|
|||
function r3u_is_cell(ui_st: UiState, src: string) -> bool {
|
||||
for k in 0 .. len(ui_st.r3u_atlas) {
|
||||
let p = ui_st.r3u_atlas[k]
|
||||
if len(src) > len(p) and src[0..len(p)] == p and src[len(p)] == 58 { return true }
|
||||
if len(src) > len(p) and sat(src, 0, p) and src[len(p)] == 58 { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
# screen_class.ludic - a Ludic component shown as a whole screen
|
||||
# a Ludic component shown as a screen: an instance at the top, keyed by its name
|
||||
function fr_class_nodes(ui_st: mut UiState, cls: UiClass) -> UiNode {
|
||||
pl_begin(ui_st, cls.name)
|
||||
lc_begin(ui_st)
|
||||
ui_st.in_build += 1
|
||||
ui_st.ui_view = null
|
||||
uc_load(ui_st, cls)
|
||||
let root = fr_root(cls.name, cls.sheet)
|
||||
let root = fr_root(ui_st, cls.name, cls.sheet)
|
||||
let use = new UiTpl
|
||||
use.tag = cls.name
|
||||
use.cls = cls
|
||||
|
|
@ -14,7 +15,7 @@ function fr_class_nodes(ui_st: mut UiState, cls: UiClass) -> UiNode {
|
|||
use.on_keys = new []string
|
||||
use.on_acts = new []UiAct
|
||||
use.kids = new []UiTpl
|
||||
let e = env_new(null)
|
||||
let e = env_new(ui_st, null)
|
||||
e.model = Value.object()
|
||||
root.env = e
|
||||
bd_class(ui_st, use, e, cls.name, root.children)
|
||||
|
|
@ -22,15 +23,10 @@ function fr_class_nodes(ui_st: mut UiState, cls: UiClass) -> UiNode {
|
|||
cs_tree(ui_st, root)
|
||||
return root
|
||||
}
|
||||
function fr_root(key: string, sheet: UiSheet) -> UiNode {
|
||||
let root = new UiNode
|
||||
function fr_root(ui_st: mut UiState, key: string, sheet: UiSheet) -> UiNode {
|
||||
let root = nd_take(ui_st)
|
||||
root.key = key
|
||||
root.tag = "screen"
|
||||
root.sheet = sheet
|
||||
root.children = new []UiNode
|
||||
root.classes = new []string
|
||||
root.akeys = new []string
|
||||
root.avals = new []string
|
||||
root.vals = new []Val
|
||||
return root
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
# it where it was taken; a press on the track jumps the thumb's middle to the pointer and holds it there.
|
||||
# the bar's parts, styled as descendants of the box, and where they are at this offset
|
||||
function sb_geo(ui_st: mut UiState, n: UiNode, off: float, room: float, most: float) -> void {
|
||||
let bar = ct_part(n, "ui-scrollbar", "")
|
||||
let bar = ct_part(ui_st, n, "ui-scrollbar", "")
|
||||
bar.tag = "div"
|
||||
bar.parent = n
|
||||
push(bar.children, ct_part(n, "ui-thumb", ""))
|
||||
push(bar.children, ct_part(ui_st, n, "ui-thumb", ""))
|
||||
cs_tree(ui_st, bar)
|
||||
ui_st.sb_bar = bar
|
||||
ui_st.sb_w = ui_px(ui_st, 6.0)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export function ui_forget(ui_st: mut UiState, screen: string) -> void {
|
|||
let vals = new []Val
|
||||
for i in 0 .. len(ui_st.st_keys) {
|
||||
let k = ui_st.st_keys[i]
|
||||
let mine = len(k) >= len(screen) and k[0..len(screen)] == screen and (len(k) == len(screen) or k[len(screen)] == 47)
|
||||
let mine = len(k) >= len(screen) and sat(k, 0, screen) and (len(k) == len(screen) or k[len(screen)] == 47)
|
||||
if not mine {
|
||||
push(keys, k)
|
||||
push(vals, ui_st.st_vals[i])
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function sheet_read(ui_st: mut UiState, f: UiFile, t: UiTpl) -> void {
|
|||
if from != "" { src = lib_text(ui_st, lib_beside(f.path, from)) }
|
||||
lss_text(ui_st, f, src)
|
||||
}
|
||||
function lss_is(path: string) -> bool { return len(path) > 4 and path[len(path) - 4..len(path)] == ".lss" }
|
||||
function lss_is(path: string) -> bool { return len(path) > 4 and sat(path, len(path) - 4, ".lss") }
|
||||
# a stylesheet file: a file with rules and imports and nothing else
|
||||
function lss_file(ui_st: mut UiState, src: string, path: string) -> UiFile {
|
||||
let f = lib_new(ui_st, path)
|
||||
|
|
@ -64,7 +64,7 @@ function lss_block(ui_st: mut UiState, f: UiFile, src: string, md: UiMedia, err:
|
|||
# @import "base.lss"; - its rules come before this file's own
|
||||
function lss_import(ui_st: mut UiState, f: UiFile, what: string) -> void {
|
||||
var p = tpl_words(what)
|
||||
if len(p) > 5 and p[0..4] == "url(" { p = p[4..len(p) - 1] }
|
||||
if len(p) > 5 and sat(p, 0, "url(") { p = p[4..len(p) - 1] }
|
||||
if len(p) >= 2 and (p[0] == 34 or p[0] == 39) { p = p[1..len(p) - 1] }
|
||||
let other = lib_file(ui_st, lib_beside(f.path, p))
|
||||
if other != null {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# text_fit.ludic - text-fit: shrink 12px draws a line that does not fit its box smaller, down to that
|
||||
# size, and cuts what still does not fit with an ellipsis; line-height (a multiple of the font size,
|
||||
# px or em) spaces a text's lines. Both are inherited, as font properties are.
|
||||
function el_text_fit(ui_st: UiState, n: UiNode, s: string) -> void {
|
||||
let parts = lss_split(tpl_words(s), 32)
|
||||
function el_text_fit(ui_st: mut UiState, n: UiNode, s: string) -> void {
|
||||
let parts = mm_split(ui_st, tpl_words(s), 32)
|
||||
nx(n).fit_min = 0.0
|
||||
if len(parts) >= 2 and parts[0] == "shrink" { nx(n).fit_min = Math.max(el_unit(ui_st, parts[1]), 1.0) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,12 +39,11 @@ function tt_under(n: UiNode, x: float, y: float) -> UiNode {
|
|||
}
|
||||
function tt_draw(ui_st: mut UiState, root: UiNode) -> void {
|
||||
if ui_st.tt_key == "" or ui_st.tt_text == "" or an_now(ui_st) - ui_st.tt_since < 0.5 { return }
|
||||
let n = ct_part(root, "ui-tooltip", "")
|
||||
let n = ct_part(ui_st, root, "ui-tooltip", "")
|
||||
n.tag = "div"
|
||||
n.parent = root
|
||||
n.children = new []UiNode
|
||||
let lines = tt_lines(ui_st.tt_text)
|
||||
for i in 0 .. len(lines) { push(n.children, ct_part(root, "ui-tooltip-text", lines[i])) }
|
||||
for i in 0 .. len(lines) { push(n.children, ct_part(ui_st, root, "ui-tooltip-text", lines[i])) }
|
||||
cs_tree(ui_st, n)
|
||||
ly_measure(ui_st, n, ui_st.ly_vp_w * 0.4, ui_px(ui_st, 16.0), root.rfg)
|
||||
let x = Math.min(ui_st.tt_x + ui_px(ui_st, 16.0), ui_st.ly_vp_x + ui_st.ly_vp_w - n.mw)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ function tpl_read(ui_st: mut UiState, x: Xml, file: string) -> UiTpl {
|
|||
for i in 0 .. len(x.akeys) {
|
||||
let k: string = x.akeys[i]
|
||||
let v: string = x.avals[i]
|
||||
if len(k) > 3 and k[0..3] == "on-" {
|
||||
if len(k) > 3 and sat(k, 0, "on-") {
|
||||
push(t.on_keys, tpl_event(k[3..len(k)]))
|
||||
push(t.on_acts, act_parse(v, err))
|
||||
} else if k == "onclick" {
|
||||
|
|
@ -68,6 +68,7 @@ function tpl_value(v: string, err: []string) -> UiExpr {
|
|||
}
|
||||
# text as a reader sees it: the ends trimmed and every run of white space one space
|
||||
function tpl_words(s: string) -> string {
|
||||
if tpl_clean(s) { return s }
|
||||
var out = ""
|
||||
var gap = false
|
||||
for i in 0 .. len(s) {
|
||||
|
|
@ -86,3 +87,15 @@ function tpl_attr(t: UiTpl, key: string) -> UiExpr {
|
|||
}
|
||||
return null
|
||||
}
|
||||
# no space at either end, no two together, no tab or line break: tpl_words would give it back as is
|
||||
function tpl_clean(s: string) -> bool {
|
||||
let n = len(s)
|
||||
if n == 0 { return true }
|
||||
if s[0] == 32 or s[n - 1] == 32 { return false }
|
||||
for i in 0 .. n {
|
||||
let c = s[i]
|
||||
if c == 9 or c == 10 or c == 13 { return false }
|
||||
if c == 32 and s[i + 1] == 32 { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@
|
|||
function el_unit(ui_st: UiState, s: string) -> float {
|
||||
let f = el_num(s)
|
||||
let n = len(s)
|
||||
if n > 3 and s[n - 3..n] == "rem" { return f * el_px(ui_st, 16.0) }
|
||||
if n > 2 and s[n - 2..n] == "em" {
|
||||
if n > 3 and sat(s, n - 3, "rem") { return f * el_px(ui_st, 16.0) }
|
||||
if n > 2 and sat(s, n - 2, "em") {
|
||||
if ui_st.el_cur == null { return f * el_px(ui_st, 16.0) }
|
||||
if ui_st.el_key == "size" and ui_st.el_cur.parent != null { return f * cs_fs(ui_st, ui_st.el_cur.parent) }
|
||||
return f * cs_fs(ui_st, ui_st.el_cur)
|
||||
}
|
||||
if n > 2 and s[n - 2..n] == "vw" { return f * ui_st.ly_vp_w / 100.0 }
|
||||
if n > 2 and s[n - 2..n] == "vh" { return f * ui_st.ly_vp_h / 100.0 }
|
||||
if n > 2 and sat(s, n - 2, "vw") { return f * ui_st.ly_vp_w / 100.0 }
|
||||
if n > 2 and sat(s, n - 2, "vh") { return f * ui_st.ly_vp_h / 100.0 }
|
||||
return el_px(ui_st, f)
|
||||
}
|
||||
# the font size an element has so far in its cascade (its own, else its parent's)
|
||||
|
|
@ -55,7 +55,7 @@ function cs_vars_in(ui_st: mut UiState, n: UiNode, v: Val) -> Val {
|
|||
var found = false
|
||||
var i = 0
|
||||
while i + 4 <= len(s) {
|
||||
if s[i..i + 4] == "var(" {
|
||||
if sat(s, i, "var(") {
|
||||
found = true
|
||||
var j = i + 4
|
||||
var depth = 1
|
||||
|
|
@ -73,7 +73,7 @@ function cs_vars_in(ui_st: mut UiState, n: UiNode, v: Val) -> Val {
|
|||
return Value.str(tpl_words(out + s[at..len(s)]))
|
||||
}
|
||||
function cs_var_ref(ui_st: mut UiState, n: UiNode, inner: string) -> string {
|
||||
let parts = lss_split(inner, 44)
|
||||
let parts = mm_split(ui_st, inner, 44)
|
||||
let v = cs_var(n, tpl_words(parts[0]))
|
||||
if v != null { return v }
|
||||
if len(parts) > 1 { return tpl_words(inner[len(parts[0]) + 1..len(inner)]) }
|
||||
|
|
|
|||
|
|
@ -71,3 +71,14 @@ function vl_binary(op: string, a: Val, b: Val) -> Val {
|
|||
# a value as text, whatever kind it is: a component's string prop given a number (label="{3}") reads
|
||||
# "3", not nothing
|
||||
export function ui_val_text(v: Val) -> string { return vl_text(v) }
|
||||
# vl_text for what is asked per element per frame: a small whole number's text comes from the table
|
||||
# made once (mm_int) rather than being written out again
|
||||
function vl_text_u(ui_st: mut UiState, v: Val) -> string {
|
||||
let k = Value.kind(v)
|
||||
if k == 1 { return mm_int(ui_st, Value.as_int(v)) }
|
||||
if k == 2 or k == 7 {
|
||||
let f = Value.as_float(v)
|
||||
if f == float(int(f)) { return mm_int(ui_st, int(f)) }
|
||||
}
|
||||
return vl_text(v)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,16 @@ import "ludic.base"
|
|||
import "ludic.physics"
|
||||
program VehiclesTest {
|
||||
numbers float
|
||||
function pose(physics_st: mut PhysicsState, id: int) -> PhysPose {
|
||||
let p = new PhysPose
|
||||
if not phys_pose(physics_st, id, p) { return null }
|
||||
return p
|
||||
}
|
||||
function wpose(physics_st: mut PhysicsState, id: int) -> PhysWalk {
|
||||
let p = new PhysWalk
|
||||
if not phys_walker_pose(physics_st, id, p) { return null }
|
||||
return p
|
||||
}
|
||||
|
||||
function fk_ground(x: float, z: float) -> float {
|
||||
if x > 10.0 { return -(x - 10.0) }
|
||||
|
|
@ -95,7 +105,7 @@ program VehiclesTest {
|
|||
fk_lake(physics_st)
|
||||
if vehicles_test_st.legs < 0 { vehicles_test_st.legs = phys_walker_add(physics_st, 0.8, 1.7, x, y, z, 40.0, 450.0) }
|
||||
phys_walker_move(physics_st, vehicles_test_st.legs, x, y, z, vx, vz, 0.0, 0.45, 40.0, dt)
|
||||
let p = phys_walker_pose(physics_st, vehicles_test_st.legs)
|
||||
let p = wpose(physics_st, vehicles_test_st.legs)
|
||||
vehicles_test_st.lx = p.x
|
||||
vehicles_test_st.ly = p.y
|
||||
vehicles_test_st.lz = p.z
|
||||
|
|
@ -120,7 +130,7 @@ program VehiclesTest {
|
|||
function fk_drift(physics_st: mut PhysicsState, vehicles_test_st: VehiclesTestState, nid: int, fx: float, fz: float) -> void { phys_force(physics_st, vehicles_test_st.hull, fx, 0.0, fz) }
|
||||
function fk_read(physics_st: mut PhysicsState, vehicles_test_st: mut VehiclesTestState, nid: int) -> bool {
|
||||
if vehicles_test_st.hull < 0 or vehicles_test_st.hull_nid != nid { return false }
|
||||
let p = phys_pose(physics_st, vehicles_test_st.hull)
|
||||
let p = pose(physics_st, vehicles_test_st.hull)
|
||||
if p == null { return false }
|
||||
vehicles_test_st.hx = p.x
|
||||
vehicles_test_st.hy = p.y
|
||||
|
|
|
|||
|
|
@ -9,14 +9,16 @@ function wl_react(wildlife_st: mut WildlifeState, a: WildAnimal, np: int, dt: fl
|
|||
return true
|
||||
}
|
||||
if a.state == WILD_WARY {
|
||||
wl_face(a, a.x + (a.x - a.sus_x), a.z + (a.z - a.sus_z), 2.6, dt)
|
||||
wl_away(a, a.sus_x, a.sus_z, 20.0)
|
||||
wl_head(a, a.tx, a.tz, 2.6, dt)
|
||||
a.speed = s.walk * 0.8
|
||||
wl_step(wildlife_st, a, dt)
|
||||
if a.timer < 0.0 { wl_wander(wildlife_st, a, 3.0 + wl_rnd(wildlife_st) * 4.0) }
|
||||
return true
|
||||
}
|
||||
if a.state == WILD_FLEE {
|
||||
wl_face(a, a.x + (a.x - WildlifeWorld.player_x(np)), a.z + (a.z - WildlifeWorld.player_z(np)), 5.0, dt)
|
||||
wl_away(a, WildlifeWorld.player_x(np), WildlifeWorld.player_z(np), 40.0)
|
||||
wl_head(a, a.tx, a.tz, 5.0, dt)
|
||||
a.speed = s.run
|
||||
wl_step(wildlife_st, a, dt)
|
||||
if a.timer < 0.0 {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ export state WildlifeState {
|
|||
wl_dice: Rng = rng_new(97)
|
||||
wl_hold: bool = false # staged: nobody thinks, everybody keeps their speed
|
||||
wl_spot_ask: WildSpot = new WildSpot
|
||||
wl_turned: int = 0 # steps refused and turned 75 degrees instead: the fallback, counted
|
||||
wl_turned_wet: int = 0 # of those, for water ahead
|
||||
wl_turned_steep: int = 0 # for a climb too steep
|
||||
}
|
||||
|
||||
function wl_sp_all(wildlife_st: WildlifeState) -> []WildSpecies {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ function wl_face(a: WildAnimal, tx: float, tz: float, rate: float, dt: float) ->
|
|||
}
|
||||
|
||||
# true when it moved; a walker is slid round a still thing (a trunk, a tent), a bird is not
|
||||
# how many steps were refused and turned from since the start: how often the way was not enough
|
||||
export function wildlife_turned(wildlife_st: WildlifeState) -> int { return wildlife_st.wl_turned }
|
||||
# of those, how many for water ahead and how many for a climb (the rest: pushed back by a still thing)
|
||||
export function wildlife_turned_wet(wildlife_st: WildlifeState) -> int { return wildlife_st.wl_turned_wet }
|
||||
export function wildlife_turned_steep(wildlife_st: WildlifeState) -> int { return wildlife_st.wl_turned_steep }
|
||||
|
||||
function wl_step(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bool {
|
||||
let d = a.speed * dt
|
||||
if d < 0.0001 { return false }
|
||||
|
|
@ -26,9 +32,13 @@ function wl_step(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bo
|
|||
}
|
||||
let nh = WildlifeWorld.ground(nx, nz)
|
||||
let moved = Math.abs(nx - a.x) + Math.abs(nz - a.z)
|
||||
if nh < WildlifeWorld.water(nx, nz) + 0.6 or nh - a.y > d * 1.2 or moved < d * 0.2 {
|
||||
let wet = nh < WildlifeWorld.water(nx, nz) + 0.6
|
||||
let steep = nh - a.y > d * 1.2
|
||||
if wet or steep or moved < d * 0.2 {
|
||||
if wet { wildlife_st.wl_turned_wet += 1 } else if steep { wildlife_st.wl_turned_steep += 1 }
|
||||
a.yaw = a.yaw + Math.deg_to_rad(75.0)
|
||||
a.timer = 0.0
|
||||
wildlife_st.wl_turned += 1
|
||||
return false
|
||||
}
|
||||
a.x = nx
|
||||
|
|
|
|||
|
|
@ -103,4 +103,23 @@ program WayTest {
|
|||
expect_near(a.tx, a.home_x + 7.0, 0.001)
|
||||
expect_near(a.tz, a.home_z + 3.0, 0.001)
|
||||
}
|
||||
|
||||
test "it runs from the player by the way too: round the wall's end, not into it" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState, way_test_st: WayTestState) {
|
||||
fk_setup(wildlife_st, wildlife_tests_fake_st)
|
||||
let a = wildlife_make(wildlife_st, 0, 0.0, 0.0, false, 0.0, 0, 1.0, 1)
|
||||
wildlife_tests_fake_st.fk_px[0] = 0.0
|
||||
wildlife_tests_fake_st.fk_pz[0] = 20.0
|
||||
wildlife_tests_fake_st.fk_hidden = true
|
||||
a.state = WILD_FLEE
|
||||
a.timer = 300.0
|
||||
var east = -100.0
|
||||
for k in 0 .. 1200 {
|
||||
wildlife_tick(wildlife_st, 0.05, 0.0)
|
||||
if a.z > -42.0 and a.z < -38.0 { east = Math.max(east, a.x) }
|
||||
}
|
||||
expect_eq(a.state, WILD_FLEE)
|
||||
expect(east > 29.0)
|
||||
expect(a.z < -45.0)
|
||||
expect(way_test_st.asked > 10)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,3 +34,20 @@ function wl_pick_target(wildlife_st: WildlifeState, a: WildAnimal, radius: float
|
|||
a.tz = WildlifeWorld.way_z()
|
||||
}
|
||||
}
|
||||
|
||||
# a target d metres straight away from (fx, fz): where it runs or backs off to, by the world's way -
|
||||
# round water and a cliff where there is one, and straight away as before where there is not
|
||||
function wl_away(a: WildAnimal, fx: float, fz: float, d: float) -> void {
|
||||
var dx = a.x - fx
|
||||
var dz = a.z - fz
|
||||
let l = Math.sqrt(dx * dx + dz * dz)
|
||||
if l < 0.01 {
|
||||
dx = -Math.sin(a.yaw)
|
||||
dz = -Math.cos(a.yaw)
|
||||
} else {
|
||||
dx = dx / l
|
||||
dz = dz / l
|
||||
}
|
||||
a.tx = a.x + dx * d
|
||||
a.tz = a.z + dz * d
|
||||
}
|
||||
|
|
|
|||
|
|
@ -479,6 +479,7 @@ namespace Value {
|
|||
alias as_float(value) = value_as_float
|
||||
alias as_str(value) = value_as_str
|
||||
alias free(value) = value_free
|
||||
alias clear(value) = value_clear
|
||||
}
|
||||
namespace Json {
|
||||
alias encode(value) = json_encode
|
||||
|
|
|
|||
|
|
@ -24,12 +24,22 @@ property Val {
|
|||
kids: []Val
|
||||
}
|
||||
|
||||
# a number, a string or a null is one record; only a list or an object carries lists (made when first
|
||||
# filled) - three allocations for every scalar was most of what a UI frame made and never gave back
|
||||
function value_new(tag: int) -> Val {
|
||||
let v = new Val
|
||||
v.tag = tag
|
||||
if tag == 5 or tag == 6 { value_lists(v) }
|
||||
return v
|
||||
}
|
||||
function value_lists(v: Val) -> void {
|
||||
if v.kids != null { return }
|
||||
v.keys = new []pointer
|
||||
v.kids = new []Val
|
||||
return v
|
||||
}
|
||||
function value_n(v: Val) -> int {
|
||||
if v.kids == null { return 0 }
|
||||
return len(v.kids)
|
||||
}
|
||||
|
||||
# --- constructors -----------------------------------------------------------
|
||||
|
|
@ -43,8 +53,13 @@ function value_float(x: float) -> Val { let v = value_new(7); v.num = float_bits
|
|||
function value_object() -> Val { return value_new(6) }
|
||||
|
||||
# --- builders (return the container, so calls chain) ------------------------
|
||||
function value_add(list: Val, item: Val) -> Val { push(list.kids, item); return list }
|
||||
function value_add(list: Val, item: Val) -> Val {
|
||||
value_lists(list)
|
||||
push(list.kids, item)
|
||||
return list
|
||||
}
|
||||
function value_put(obj: Val, key: pointer, item: Val) -> Val {
|
||||
value_lists(obj)
|
||||
var i = 0
|
||||
while i < len(obj.keys) { if obj.keys[i] == key { obj.kids[i] = item; return obj }; i += 1 }
|
||||
push(obj.keys, key); push(obj.kids, item)
|
||||
|
|
@ -53,7 +68,13 @@ function value_put(obj: Val, key: pointer, item: Val) -> Val {
|
|||
|
||||
# --- accessors --------------------------------------------------------------
|
||||
function value_kind(v: Val) -> int { return v.tag }
|
||||
function value_count(v: Val) -> int { return len(v.kids) }
|
||||
function value_count(v: Val) -> int { return value_n(v) }
|
||||
# a list or an object emptied in place, its room kept - for one filled again every frame
|
||||
function value_clear(v: Val) -> void {
|
||||
if v.kids == null { return }
|
||||
List.clear(v.kids)
|
||||
List.clear(v.keys)
|
||||
}
|
||||
function value_as_int(v: Val) -> int {
|
||||
if v.tag == 7 { return int(float_from_bits(v.num)) }
|
||||
return v.num
|
||||
|
|
@ -66,21 +87,21 @@ function value_as_float(v: Val) -> float {
|
|||
}
|
||||
function value_as_str(v: Val) -> string { if v.txt == null { return "" }; return v.txt }
|
||||
function value_at(list: Val, i: int) -> Val {
|
||||
if i < 0 or i >= len(list.kids) { return value_null() }
|
||||
if i < 0 or i >= value_n(list) { return value_null() }
|
||||
return list.kids[i]
|
||||
}
|
||||
function value_key_at(obj: Val, i: int) -> string {
|
||||
if i < 0 or i >= len(obj.keys) { return "" }
|
||||
if i < 0 or i >= value_n(obj) { return "" }
|
||||
return obj.keys[i]
|
||||
}
|
||||
function value_get(obj: Val, key: pointer) -> Val {
|
||||
var i = 0
|
||||
while i < len(obj.keys) { if obj.keys[i] == key { return obj.kids[i] }; i += 1 }
|
||||
while i < value_n(obj) { if obj.keys[i] == key { return obj.kids[i] }; i += 1 }
|
||||
return value_null()
|
||||
}
|
||||
function value_has(obj: Val, key: pointer) -> int {
|
||||
var i = 0
|
||||
while i < len(obj.keys) { if obj.keys[i] == key { return 1 }; i += 1 }
|
||||
while i < value_n(obj) { if obj.keys[i] == key { return 1 }; i += 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +153,7 @@ function json_encode(v: Val) -> string {
|
|||
if v.tag == 5 {
|
||||
var out = "["
|
||||
var i = 0
|
||||
while i < len(v.kids) {
|
||||
while i < value_n(v) {
|
||||
if i > 0 { out += "," }
|
||||
out += json_encode(v.kids[i])
|
||||
i += 1
|
||||
|
|
@ -141,7 +162,7 @@ function json_encode(v: Val) -> string {
|
|||
}
|
||||
var out = "{"
|
||||
var i = 0
|
||||
while i < len(v.keys) {
|
||||
while i < value_n(v) {
|
||||
if i > 0 { out += "," }
|
||||
out = out + json_quote(v.keys[i]) + ":" + json_encode(v.kids[i])
|
||||
i += 1
|
||||
|
|
@ -176,9 +197,11 @@ function json_parse(s: pointer) -> Val {
|
|||
# a node or hold a literal, is not for this (Ludic has no collector; plan 23 of maroon-lake).
|
||||
function value_free(v: Val) -> void {
|
||||
if v == null { return }
|
||||
for i in 0 .. len(v.kids) { value_free(v.kids[i]) }
|
||||
free(v.kids)
|
||||
free(v.keys)
|
||||
if v.kids != null {
|
||||
for i in 0 .. len(v.kids) { value_free(v.kids[i]) }
|
||||
free(v.kids)
|
||||
free(v.keys)
|
||||
}
|
||||
free(v)
|
||||
}
|
||||
function json_free(v: Val) -> void { value_free(v) }
|
||||
|
|
|
|||
|
|
@ -608,6 +608,13 @@ function headless_case(path: pointer, exp: pointer, label: pointer) -> void {
|
|||
let got = capture_line(`{out} < /dev/null`)
|
||||
if got == exp { ok(label) } else { bad2(label, `got [{got}] want [{exp}]`) }
|
||||
}
|
||||
# a program built headless whose verdict is one of its lines (the renderer logs before it)
|
||||
function headless_line_case(path: pointer, exp: pointer, label: pointer) -> void {
|
||||
let out = `{tmp_dir()}/h_{flat(path)}`
|
||||
if not shq(`bin/ludicc --headless examples/{path}.ludic -o {out} > {out}.log 2>&1`) { bad2(label, capture_line(`grep -i error {out}.log | head -1`)); return }
|
||||
let got = capture_line(`{out} < /dev/null 2>&1 | grep -c '^{exp}$'`)
|
||||
if got == "1" { ok(label) } else { bad2(label, capture_line(`{out} < /dev/null 2>&1 | grep -i fail | head -1`)) }
|
||||
}
|
||||
# ludic migrate state and components: the header names what every member needs, a field read in a
|
||||
# member is not edited, a module named like a package keeps a state of its own, and the program
|
||||
# runs as it did
|
||||
|
|
@ -1045,6 +1052,7 @@ function cmd_dev_test() -> int {
|
|||
reject_case("rejected/state_component_ro", "look is read-only here (look: Look)", "a component's state is read-only unless its header says mut")
|
||||
controller_case("state/component", "", " #10 big 10 1", "component.ludic (0.S: a component's header names its states; its getters, functions and events take them and the template never sees them)")
|
||||
headless_case("rendering/ui_render3d", "ok", "ui_render3d.ludic (ludic.ui's render3d backend builds against the renderer)")
|
||||
headless_line_case("rendering/release", "RELEASE OK", "release.ludic (model_release: meshes go, a shared texture stays until its last user goes)")
|
||||
migrate_component_case()
|
||||
migrate_foreign_case()
|
||||
reject_case("rejected/runtime_type_clash", "PadButton is the runtime's enum", "a program's type named like one of the runtime's is refused")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue