luv

Workshop wiki

paper.lisp

mcclim/paper.lisp

system luvcraft/paper · 32 definitions · on GitHub

A sheet of PDF paper, standing in the luvcraft world.

The page is split the way #S8LIJP says to split it: the sheet is one retained analytic primitive and the type is world geometry. Both enter the game's final pass directly; there is no pane raster or texture blit.

The text is not in the texture at all. MuPDF gives back each typeset line with its baseline, its size, and the font it was set in; those are shaped through Slug and emitted as world-space glyph instances on the sheet's own plane, the same way a terminal wall emits its screen. Slug evaluates the outlines in the fragment shader at whatever resolution the pixel has, so the page is as sharp far away as it is up close -- which is the whole point, and which drawing the text into the texture could not do at any texture size (#I3G0S7).

in-package#:mcluv
defparameter*paper-texture-width*1200

How many texture pixels wide a sheet is drawn.

Sized for reading distance, which is the only distance at which a page of ten point body text is legible at all -- on paper too. At 500 the body landed on ten pixels a line and broke up; much past this it is being minified instead, and the mirror texture has no mipmaps, so that aliases rather than softens.

defparameter*paper-texture-height*680

How many tall. open-luvcraft-paper rebinds this to the first page's own proportions before making the frame, so the quad hung in the world is the sheet itself -- there is no surround, and so nothing to decide what to put in it.

defparameter*paper-margin*0

How much texture is left around the sheet. Zero, because the texture is cut to the page: the sheet is the whole quad and its edge is the page edge.

defparameter*paper-document-pathname*
merge-pathnames"build/tool-being.pdf"
asdf:system-source-directory"luv"
"The document a sheet shows when nobody said which."
defparameter*paper-ink-lift*0.004

How far off the sheet the type stands, in world units.

Ink on paper is not coplanar with it. Nor can it be here: glyphs at exactly the sheet's depth lose the depth test against the sheet and come out hollow.

defparameter*paper-text-ink*
make-rgb-color0.100.090.08
defparameter*paper-muted-ink*
make-rgb-color0.520.500.46
define-application-frameluvcraft-paper
document:initarg:document:accessorpaper-document
page:initform0:accessorpaper-page

The page's own size in points and its lines, refetched when the page turns. Extraction is a few milliseconds and the page turns rarely, so there is nothing to be gained by doing it anywhere but here.

page-width:initform1.0:accessorpaper-page-width
page-height:initform1.0:accessorpaper-page-height
runs:initform'
:accessorpaper-runs
painted:initformnil:accessorpaper-painted
:menu-barnil
:panes
paper
make-pane'paper-pane:background+transparent-ink+:default-text-style
make-text-style:serifnil:normal
:width*paper-texture-width*:height*paper-texture-height*:min-width*paper-texture-width*:min-height*paper-texture-height*:max-width*paper-texture-width*:max-height*paper-texture-height*
:layouts
defaultpaper
defunload-paper-page

Measure the current page and pull its lines out of the document.

let
document
paper-documentframe
number
paper-pageframe
multiple-value-bind
setf
paper-page-widthframe
paper-page-heightframe
setf
paper-runsframe
handler-case
error
'
frame
defunpaper-sheet-geometry
frametexture-widthtexture-height

Where the sheet sits in the texture, and how many texture pixels a point is.

Measured from the texture the pane actually has rather than from the parameter it was asked for: the first page decides the shape, later pages of the same document need not be the same shape, and a page that is not gets centred with paper around it rather than stretched.

let*
available-height
-texture-height
available-width
-texture-width
scale
min
/available-height
paper-page-heightframe
/available-width
paper-page-widthframe
width
*scale
paper-page-widthframe
height
*scale
paper-page-heightframe
values
/
-texture-widthwidth
2.0
/
-texture-heightheight
2.0
widthheightscale
defmethodhandle-repaint
region

Paint the sheet, and only the sheet. The type is world geometry.

declare
ignoreregion
let
frame
pane-framepane
with-sheet-medium
mediumpane
with-bounding-rectangle*
lefttoprightbottom
pane
draw-analytic-rounded-rectangle*mediumlefttoprightbottom:radius4:ink
make-linear-gradient0top0bottom
make-rgb-color0.9850.9800.960
make-rgb-color0.9000.8950.870
defunpaper-paint-state
list
paper-pageframe
length
paper-runsframe
defunrepaint-paper
let
mirror
sheet-direct-mirror
frame-top-level-sheetframe
check-typemirrorluv-gpu-mirror
frame
defunturn-paper-page
framedelta

Move delta pages, staying inside the document.

let*
wanted
max0
min
1-count
+
paper-pageframe
delta
unless
=wanted
paper-pageframe
setf
paper-pageframe
wanted
t

Setting the page as geometry

One text run holds every line of the page. The whole page shares one glyph atlas and one instanced draw, which is why the lines are shaped separately but their instance records are concatenated: the atlas is a property of the glyph set, and a set assembled from the whole page is one binding rather than one per line.

defparameter*paper-faces*'
:italic."DejaVuSerifCondensed-Italic.ttf"
:bold."DejaVuSerifCondensed-Bold.ttf"
:regular."DejaVuSerifCondensed.ttf"

The faces a page is set in.

Condensed on purpose. The documents this reads are set in Palatino-like faces and the ordinary DejaVu serif is about a third wider, which is what forced the old raster path to squeeze every line to make it fit. A condensed face lands close enough that the lines simply sit where the document put them.

defunpaper-face-pathname
font-name

The face to set a run in, from what the document says it used.

let
name
orfont-name""
cl-dejavu:font-pathname
cdr
assoc
cond
search"Italic"name
:italic
search"Oblique"name
:italic
search"Bold"name
:bold
t:regular
*paper-faces*
defunpaper-plane
overlay

The sheet's top-left corner and the unit vectors that span it.

Returns the corner, a unit vector along the page's own left-to-right, a unit vector along its top-to-bottom, and how many world units a PDF point is.

let*
center
widget-overlay-centeroverlay
right
widget-overlay-right-axisoverlay
up
widget-overlay-up-axisoverlay
frame
widget-overlay-frameoverlay
half-width
right-unit
vec:vec3-scaleright
/1.0
max1e-6half-width

UP-AXIS points down the texture, so the top-left corner is one half-axis back along both. The type also stands a fraction of a centimetre off the sheet: glyphs exactly coplanar with the paper lose the depth test against it and come out hollow, which is the same reason a terminal wall lifts its glyph run off its blocks.

corner
add-scaled-vectorcenterright-1.0up-1.0
widget-overlay-normal-axisoverlay
*paper-ink-lift*
points-per-world
/
*2.0half-width
max1.0
paper-page-widthframe
valuescornerright-unitdown-unitpoints-per-world
defunpaper-run-instances
overlayrunatlasglyphsshapedfont-loader

Instance records placing one shaped line on the sheet at its own baseline.

multiple-value-bind
cornerright-unitdown-unitscale
multiple-value-bind
min-xmin-ymax-xmax-y
luv.slug:slug-text-extentsglyphsshapedfont-loader
let*
em
*scale
luv.mupdf:text-run-sizerun
text-up
vec:vec3-scaledown-unit-1.0

Where the pen starts, in world space.

pen
add-scaled-vectorcornerright-unit
*scale
luv.mupdf:text-run-baseline-xrun
down-unit
*scale
luv.mupdf:text-run-baseline-yrun

make-world-text-instances centres a run on its own inked extents, so the centre that puts the pen where it belongs is the pen displaced by half of those extents.

center
add-scaled-vectorpenright-unit
*em
/
+min-xmax-x
2.0
text-up
*em
/
+min-ymax-y
2.0
luvcraft::make-world-text-instancesglyphsatlascenterright-unittext-upemmin-xmin-ymax-xmax-y:ink'
0.090.080.07
defunpaper-shaped-lines
overlay

Shape every line of the current page and place it on the sheet.

Returns the concatenated instance data, every glyph placement, and the atlas they share, or NIL when the page has no drawable text.

let*
frame
widget-overlay-frameoverlay
cache
paper-glyph-cacheoverlay
shapings'

Shape first, so the atlas is packed once for the whole page.

dolist
run
paper-runsframe
let
string
luv.mupdf:text-run-stringrun
when
plusp
length
string-trim" "string
let*
font
paper-face-pathname
luv.mupdf:text-run-fontrun
zpb-ttf:with-font-loader
loaderfont
let
glyphs
whenglyphs
push
listrunshapedglyphsfont
shapings
setfshapings
nreverseshapings
whenshapings
let*
all-glyphs
loopfor
nilnilglyphsnil
inshapingsappendglyphs
atlas
records
loopfor
runshapedglyphsfont
inshapingscollect
zpb-ttf:with-font-loader
loaderfont
paper-run-instancesoverlayrunatlasglyphsshapedloader
total
reduce#'+records:key#'length
data
make-arraytotal:element-type'single-float
cursor0
dolist
recordrecords
replacedatarecord:start1cursor
incfcursor
lengthrecord
valuesdataall-glyphsatlas

The overlay

defclassluvcraft-paper-overlay
glyph-cache:initformnil:accessorpaper-glyph-cache
text-run:initformnil:accessorpaper-text-run
text-generation:initformnil:accessorpaper-text-generation
frame-bind-groups:initform
make-hash-table:test#'eq
:readerpaper-frame-bind-groups
defunclear-paper-frame-bind-groups
overlay
maphash
lambda
framegroup
declare
ignoreframe
ignore-errors
paper-frame-bind-groupsoverlay
clrhash
paper-frame-bind-groupsoverlay
defunensure-paper-text-run
overlaysession

Build or republish the page's glyph run when the page has changed.

Called at a frame boundary rather than from inside the pass: this creates pipelines and buffers, and the pass is no place to do that.

let
frame
widget-overlay-frameoverlay
unless
equal
paper-text-generationoverlay
setf
paper-text-generationoverlay
let
device
luvcraft::luvcraft-session-devicesession
unless
paper-glyph-cacheoverlay
setf
paper-glyph-cacheoverlay
multiple-value-bind
dataglyphsatlas
cond
nulldata
nil
paper-text-runoverlay
multiple-value-bind
runatlas-changed-p
luvcraft::replace-world-text-run-instances
paper-text-runoverlay
device"page"glyphsatlasdata
declare
ignorerun

A new atlas is a new binding; the old groups name the old textures and have to go.

whenatlas-changed-p
t

The scene pass draws into the session's colour texture, not into the swapchain surface; a pipeline built for the surface format is rejected as an incompatible render pass.

setf
paper-text-runoverlay
luvcraft::make-world-text-run-from-instancesdevice
luv:gpu-texture-format
"page"nilglyphsatlas
widget-overlay-centeroverlay
1.0data:label"PDF page Slug text"
paper-text-runoverlay
defunpaper-frame-bind-group
overlayrunframedevice
or
gethashframe
paper-frame-bind-groupsoverlay
setf
gethashframe
paper-frame-bind-groupsoverlay
aref
luvcraft::make-world-text-frame-bind-groupsrundevice
luvcraft::luvcraft-frame-uniform-bufferframe
0
luv:zdefmethod
sessionpasssurface-texture
"Draw the sheet, then set the page on it."
let*
viewport-size
luv:canvas-extent
luvcraft::luvcraft-session-contextsession
state
world-device-clip-stateoverlaysession
firstviewport-size
secondviewport-size

Replay the retained analytic sheet before its independently shaped Slug text. Clear the automatic :AFTER replay marker so the paper cannot be drawn over its own ink.

prepare-direct-widget-overlayoverlaysessionsurface-texturestate
encode-direct-gpu-mirroroverlaypass
widget-overlay-render-target-textureoverlay
state:frame-texturesurface-texture
setf
widget-overlay-render-stateoverlay
nil

The type is a second draw in the same pass, in world space, sharing the scene's own frame uniform rather than the compositor's.

alexandria:when-let
run
paper-text-runoverlay
let*
device
luvcraft::luvcraft-session-devicesession
glyphs
luvcraft::world-text-run-glyphsrun
when
plusp
lengthglyphs
luv:set-vertex-bufferpass0
luvcraft::world-text-run-vertex-bufferrun
luv:set-vertex-bufferpass1
luvcraft::world-text-run-instance-bufferrun
luv:drawpass6
lengthglyphs
overlay
luv:zdefmethod
let
frame
widget-overlay-frameoverlay
ensure-paper-text-runoverlaysession
overlay
defmethodluvcraft:handle-luvcraft-focus-event
declare
ignorecanvas
let
frame
widget-overlay-frameoverlay
case
luv:canvas-key-event-key-nameevent
:tabnil
:right:down:page-down:space
t
:left:up:page-up
t
:home
turn-paper-pageframemost-negative-fixnum
t
:end
turn-paper-pageframemost-positive-fixnum
t
tt
defmethodluvcraft:handle-luvcraft-overlay-event

A wheel over the sheet turns pages, one notch at a time.

declare
ignoresessioncanvas
when
luvcraft-widget-texture-coordinateoverlay
luv:canvas-pointer-event-xevent
luv:canvas-pointer-event-yevent
let
scroll
luv:canvas-pointer-event-scroll-yevent
unless
zeropscroll
turn-paper-page
widget-overlay-frameoverlay
if
pluspscroll
-11
t
defmethodluvcraft:handle-luvcraft-overlay-event

Clicking the right half goes forward, the left half back.

declare
ignoresessioncanvas
alexandria:when-let
uv
luvcraft-widget-texture-coordinateoverlay
luv:canvas-pointer-event-xevent
luv:canvas-pointer-event-yevent
when
eq:left
luv:canvas-pointer-event-buttonevent
turn-paper-page
widget-overlay-frameoverlay
if
<
firstuv
0.5
-11
t
defmethodluvcraft:release-luvcraft-overlay
alexandria:when-let
run
paper-text-runoverlay
setf
paper-text-runoverlay
nil
let
frame
widget-overlay-frameoverlay
ignore-errors
call-next-method

Mounting it

defunopen-luvcraft-paper
session&key
width1.25
right-offset0.0

Hang pathname in front of session's camera as a sheet standing in the world.

A sheet of paper is not a screen bolted to a wall, so this does not go through the terminal's display modes: it is its own object, placed where the player is looking and focused with TAB like anything else.

let*
port
find-port:server-path'
:luv-gpu
manager
or
first
climi::frame-managersport
make-instance'luv-frame-manager:portport

The texture is cut to the first page's proportions, so the quad hung in the world is the sheet and there is no surround to fill. The size is passed to the frame rather than bound around it: the layout is consulted somewhere the binding does not reach, and a page-shaped parameter that quietly failed to apply is how the sheet came out landscape.

texture-height
multiple-value-bind
page-widthpage-height
max64
round
**paper-texture-width*
/page-height
max1.0page-width
frame
let
*embedded-mirror-target*
luvcraft:luvcraft-session-canvassession
*embedded-mirror-context*
luvcraft::luvcraft-session-contextsession
*embedded-mirror-device*
luvcraft::luvcraft-session-devicesession
make-application-frame'luvcraft-paper:frame-managermanager:enablet:width*paper-texture-width*:heighttexture-height:documentdocument
setf
frame-pretty-nameframe
"paper"
let*
mirror
sheet-direct-mirror
frame-top-level-sheetframe
source-size
multiple-value-list
aspect
/
firstsource-size
secondsource-size
camera
luvcraft:luvcraft-session-camerasession
camera-position
luvcraft:camera-positioncamera
multiple-value-bind
rightignored-upforward
declare
ignoreignored-up
let
overlay
make-instance'luvcraft-paper-overlay:sessionsession:frameframe:mirrormirror:center
add-scaled-vectorcamera-positionforwarddistancerightright-offset
:right-axis:up-axis
vec:make-vec30.0
-
/widthaspect2.0
0.0
:normal-axis
vec:vec3-scaleforward-1.0
:height-scale0.0
setf
mirror-compositormirror
overlay
overlay
defunclose-luvcraft-paper
overlay

Take a sheet down and close the document behind it.

check-typeoverlayluvcraft-paper-overlay
luvcraft:remove-luvcraft-overlay
widget-overlay-sessionoverlay
overlay
nil