Files
pdf-odin-llm/shading.odin
T
2026-09-25 23:32:29 +02:00

1533 lines
49 KiB
Odin

package pdf
import "core:math"
// Shadings (ISO 32000-2, 8.7.4.5). Types 1-3 are evaluated per pixel from a
// function; types 4-7 are meshes that are decomposed into triangles.
//
// HOW A SHADING REACHES THE PIXELS
//
// Everything here paints through `blend_span_rgba8`, one horizontal run at a
// time, in straight RGBA8 -- the same road an image takes, so a shading, an
// image and a fill all round identically and a page cannot show a seam where
// one meets another. Runs are assembled in `Shade_Ctx.row`, one scratch row as
// wide as the clip, and a pixel the shading does not cover is left with alpha
// zero and skipped rather than painted transparent, because "not painted" and
// "painted with alpha 0" differ under every blend mode but Normal.
//
// Types 1-3 walk the clip box row by row, mapping each pixel centre back
// through the inverse CTM into the shading's own space. Types 4-7 read
// triangles out of the mesh stream and scan-convert each one with a top-left
// fill rule at pixel centres. That rule is not decoration: adjacent triangles
// share an edge exactly, and a pixel centre landing on a shared edge must be
// claimed by exactly one of them or the mesh grows either a bright seam (the
// pixel painted twice, visible whenever alpha < 1) or a dark one (painted by
// neither). Triangles are drawn without anti-aliasing for the same reason --
// two anti-aliased edges meeting would composite twice and leave a lighter
// line down every shared edge. Triangles that genuinely overlap -- which only
// a self-intersecting mesh produces -- are composited in the order the stream
// lists them, so the later one shows, which is what the format asks for; with
// a constant alpha below 1 the overlap is painted twice, and correcting that
// would mean rendering the whole shading into a buffer of its own for a case
// no real file contains.
//
// WHAT IS NOT DONE HERE
//
// /Background is parsed but never painted. It applies only when a shading is
// used as a pattern's fill, and even there only outside the shading's own
// extent; no viewer this library was checked against paints it, and painting
// it would make `sh` disagree with `scn` for the same shading dictionary.
// /AntiAlias is likewise parsed and ignored -- it is a hint, and the mesh
// rasteriser's fill rule is chosen for seam-freedom rather than smoothness.
Shading_Kind :: enum u8 {
Function_Based = 1,
Axial = 2,
Radial = 3,
Free_Triangle = 4,
Lattice_Triangle = 5,
Coons = 6,
Tensor = 7,
}
Shading :: struct {
kind: Shading_Kind,
cs: ^Colorspace,
// One function producing cs.n outputs, or cs.n functions each producing
// one. Both spellings are legal and both occur.
funcs: []^Function,
// /Background, painted outside the shading's own extent when the shading
// is used as a pattern (it is ignored by the `sh` operator).
background: []f64,
has_background: bool,
// /BBox in the shading's own space.
bbox: Rect,
has_bbox: bool,
antialias: bool,
// Type 1.
domain: [4]f64,
mtx: Matrix,
// Types 2 and 3: /Coords is 4 numbers for axial, 6 for radial.
coords: [6]f64,
// /Domain, defaulting to [0 1].
t0, t1: f64,
extend: [2]bool,
// Types 4-7: the mesh stream and its sample layout.
stream: ^Stream,
bits_per_coord: int,
bits_per_component: int,
bits_per_flag: int,
decode: []f64,
// Type 5 only.
vertices_per_row: int,
// Colour values per mesh vertex: 1 when /Function maps a parametric value,
// otherwise one per component of `cs`. It fixes the size of every vertex
// in the stream, so it is resolved once at load time.
n_comp: int,
}
// ---------------------------------------------------------------------------
// Limits
//
// Every one of these bounds a number that came out of a file.
// ---------------------------------------------------------------------------
// SHADE_LUT_SIZE is how finely the colour function of an axial or radial
// shading is sampled. The function is 1-D there, so sampling it once per
// entry and looking the colour up per pixel turns a type 4 PostScript
// calculator running several million times into 256 runs. 256 entries put the
// error below the 8-bit quantisation the output has anyway.
@(private)
SHADE_LUT_SIZE :: 256
// Vertices one mesh stream may yield. A megabyte of 16-bit vertices is already
// far past any real gradient mesh.
@(private)
MAX_MESH_VERTICES :: 1 << 20
// /VerticesPerRow. Two is the minimum that forms a triangle; the cap is what
// stops a four-byte integer from sizing an allocation.
@(private)
MAX_MESH_ROW :: 1 << 14
@(private)
MAX_MESH_TRIANGLES :: 1 << 20
@(private)
MAX_MESH_PATCHES :: 1 << 16
// Steps a patch is subdivided into along each parameter. 32 gives 2048
// triangles for the largest patch, which is smooth well past 300 dpi.
@(private)
MAX_PATCH_SUBDIV :: 32
// Device pixels a patch aims to put between two subdivision steps.
@(private)
PATCH_STEP_PIXELS :: 3.0
// Pixels one shading may examine. This is not a quality knob -- a full page at
// 600 dpi is under 35 million -- it is the stop on a mesh built to make the
// rasteriser walk the clip a thousand times over.
@(private)
SHADE_PIXEL_FUEL :: 1 << 26
// ---------------------------------------------------------------------------
// Loading
// ---------------------------------------------------------------------------
// shading_load builds the shading `obj` describes: a dictionary for types 1-3,
// a stream for types 4-7 (whose dictionary carries the same entries).
//
// `res` is the /Resources dictionary the shading was named from, needed only
// to resolve a colour space given as a bare name.
//
// The result is owned by the Document and must not be freed. Nothing is
// cached: unlike a colour space or a function, a shading is built once per
// `sh` operator or once per pattern, and the mesh stream it points at is
// decoded lazily by `stream_data`, which does its own caching.
shading_load :: proc(doc: ^Document, obj: Object, res: ^Dict) -> (out: ^Shading, err: Error) {
if doc == nil {
return nil, .Invalid_Argument
}
o := resolve(doc, obj)
d, has_dict := to_dict(o)
if !has_dict {
return nil, .Bad_Shading
}
st := dict_int(doc, d, "ShadingType", 0)
if st < 1 || st > 7 {
return nil, .Bad_Shading
}
sh := new(Shading, doc.allocator)
sh.kind = Shading_Kind(u8(st))
cs, cs_err := colorspace_load(doc, dict_get(doc, d, "ColorSpace"), res)
// A shading whose colour space will not load cannot be guessed at: for a
// mesh the component count is what fixes the size of every vertex in the
// stream, so a wrong guess does not degrade, it garbles.
if cs_err != .None || cs == nil || cs.n < 1 || cs.n > MAX_COLOR_COMPONENTS {
return nil, .Bad_Shading
}
// 8.7.4.5.1: the colour space of a shading shall not be a Pattern space.
if cs.kind == .Pattern {
return nil, .Bad_Shading
}
sh.cs = cs
sh.funcs = shade_load_functions(doc, dict_get(doc, d, "Function"))
// Types 1-3 have no colour of their own; without a function there is
// nothing to paint with.
if len(sh.funcs) == 0 && (sh.kind == .Function_Based || sh.kind == .Axial || sh.kind == .Radial) {
return nil, .Bad_Shading
}
if ba := dict_array(doc, d, "Background"); array_len(ba) > 0 {
n := min(array_len(ba), cs.n)
bg := make([]f64, n, doc.allocator)
for i in 0 ..< n {
bg[i] = clamp_finite(array_num(doc, ba, i), 0)
}
sh.background = bg
sh.has_background = true
}
if bb, bb_ok := object_to_rect(doc, dict_get(doc, d, "BBox")); bb_ok {
sh.bbox = bb
sh.has_bbox = true
}
sh.antialias = dict_bool(doc, d, "AntiAlias")
switch sh.kind {
case .Function_Based:
shade_load_type1(doc, sh, d)
case .Axial, .Radial:
shade_load_axial_radial(doc, sh, d) or_return
case .Free_Triangle, .Lattice_Triangle, .Coons, .Tensor:
stm, is_stream := to_stream(o)
if !is_stream {
return nil, .Bad_Shading
}
sh.stream = stm
shade_load_mesh(doc, sh, d) or_return
}
return sh, .None
}
// shade_load_functions reads /Function, which is either one function with as
// many outputs as the colour space has components or an array of that many
// one-output functions. Both spellings are legal and both occur, so the array
// is what is stored and the count is what tells them apart.
//
// The array form is INDEXED BY COMPONENT, so its length and the position of
// every entry in it are load-bearing: a sub-function that will not load holds
// its slot as nil rather than being dropped. An array in which nothing loads
// comes back empty, which for types 1-3 is what makes the shading fail.
@(private)
shade_load_functions :: proc(doc: ^Document, obj: Object) -> []^Function {
if obj == nil {
return nil
}
if a, is_array := to_array(obj); is_array {
count := min(array_len(a), MAX_COLOR_COMPONENTS)
if count == 0 {
return nil
}
out, aerr := make([]^Function, count, doc.allocator)
if aerr != nil {
return nil
}
any := false
for i in 0 ..< count {
f, err := function_load(doc, array_get(doc, a, i))
if err != .None || f == nil {
// One broken sub-function must not lose the others, and it
// must not move them either: the slot is left nil so that
// every surviving function stays on the component the file
// wrote it for, and only the component this one would have
// supplied reads as zero. Dropping the entry instead would
// shift every later function down one index and repaint the
// whole shading in the wrong colours.
add_repair(doc, .Bad_Resource, 0)
continue
}
out[i] = f
any = true
}
if !any {
return nil
}
return out
}
f, err := function_load(doc, obj)
if err != .None || f == nil {
return nil
}
one := make([]^Function, 1, doc.allocator)
one[0] = f
return one
}
@(private)
shade_load_type1 :: proc(doc: ^Document, sh: ^Shading, d: ^Dict) {
sh.domain = {0, 1, 0, 1}
if da := dict_array(doc, d, "Domain"); array_len(da) >= 4 {
dom: [4]f64
ok := true
for i in 0 ..< 4 {
dom[i] = array_num(doc, da, i)
if !is_finite(dom[i]) {
ok = false
}
}
// A reversed or non-finite /Domain is not worth failing the shading
// over; normalising it paints the same region the file meant.
if ok {
sh.domain = {
min(dom[0], dom[1]), max(dom[0], dom[1]),
min(dom[2], dom[3]), max(dom[2], dom[3]),
}
}
}
sh.mtx = dict_matrix(doc, d, "Matrix")
}
@(private)
shade_load_axial_radial :: proc(doc: ^Document, sh: ^Shading, d: ^Dict) -> Error {
need := sh.kind == .Axial ? 4 : 6
ca := dict_array(doc, d, "Coords")
if array_len(ca) < need {
return .Bad_Shading
}
for i in 0 ..< need {
sh.coords[i] = array_num(doc, ca, i)
if !is_finite(sh.coords[i]) {
return .Bad_Shading
}
}
sh.t0, sh.t1 = 0, 1
if da := dict_array(doc, d, "Domain"); array_len(da) >= 2 {
a := array_num(doc, da, 0)
b := array_num(doc, da, 1)
if is_finite(a) && is_finite(b) {
sh.t0, sh.t1 = a, b
}
}
if ea := dict_array(doc, d, "Extend"); array_len(ea) >= 2 {
sh.extend[0], _ = to_bool(array_get(doc, ea, 0))
sh.extend[1], _ = to_bool(array_get(doc, ea, 1))
}
return .None
}
@(private)
shade_load_mesh :: proc(doc: ^Document, sh: ^Shading, d: ^Dict) -> Error {
bpc := int(dict_int(doc, d, "BitsPerCoordinate", 0))
switch bpc {
case 1, 2, 4, 8, 12, 16, 24, 32:
sh.bits_per_coord = bpc
case:
return .Bad_Shading
}
bpcomp := int(dict_int(doc, d, "BitsPerComponent", 0))
switch bpcomp {
case 1, 2, 4, 8, 12, 16:
sh.bits_per_component = bpcomp
case:
return .Bad_Shading
}
if sh.kind != .Lattice_Triangle {
bpf := int(dict_int(doc, d, "BitsPerFlag", 0))
switch bpf {
case 2, 4, 8:
sh.bits_per_flag = bpf
case:
return .Bad_Shading
}
}
// With a /Function the stream carries one parametric value per vertex, not
// one value per colour component (8.7.4.5.5).
sh.n_comp = len(sh.funcs) > 0 ? 1 : sh.cs.n
need := 4 + 2 * sh.n_comp
da := dict_array(doc, d, "Decode")
if array_len(da) < need {
return .Bad_Shading
}
dec := make([]f64, need, doc.allocator)
for i in 0 ..< need {
dec[i] = array_num(doc, da, i)
if !is_finite(dec[i]) {
return .Bad_Shading
}
}
sh.decode = dec
if sh.kind == .Lattice_Triangle {
vpr := int(dict_int(doc, d, "VerticesPerRow", 0))
if vpr < 2 || vpr > MAX_MESH_ROW {
return .Bad_Shading
}
sh.vertices_per_row = vpr
}
return .None
}
// ---------------------------------------------------------------------------
// Colour
// ---------------------------------------------------------------------------
// shade_components_to_rgb converts raw colour components -- the ones a mesh
// vertex carries when there is no /Function -- through the shading's space.
@(private)
shade_components_to_rgb :: proc(sh: ^Shading, comps: []f64) -> [3]f64 {
c := Color{n = sh.cs.n}
for i in 0 ..< min(sh.cs.n, len(comps)) {
c.v[i] = comps[i]
}
return colorspace_to_rgb(sh.cs, c)
}
// shade_eval runs the shading's function(s) at `in_` and converts the result
// to RGB. `in_` is one parametric value for types 2-7 and two coordinates for
// type 1.
@(private)
shade_eval :: proc(sh: ^Shading, in_: []f64) -> [3]f64 {
comps: [MAX_COLOR_COMPONENTS]f64
n := sh.cs.n
switch len(sh.funcs) {
case 0:
// Only reachable for a mesh whose colours come from the stream; the
// caller uses shade_components_to_rgb there.
return [3]f64{0, 0, 0}
case 1:
// One function supplying every component: the array spelling with a
// single entry means the same thing as the bare one, and both occur.
function_eval(sh.funcs[0], in_, comps[:n])
case:
one: [1]f64
for i in 0 ..< min(len(sh.funcs), n) {
// A nil slot is a sub-function that would not load. Its component
// stays zero; the ones around it keep their own indices.
if sh.funcs[i] == nil {
continue
}
function_eval(sh.funcs[i], in_, one[:])
comps[i] = one[0]
}
}
return shade_components_to_rgb(sh, comps[:n])
}
// Shade_Lut is the colour of an axial or radial shading sampled across its
// /Domain. Entry i is the colour at t0 + (i/(N-1)) * (t1 - t0).
@(private)
Shade_Lut :: struct {
rgb: [SHADE_LUT_SIZE][3]f64,
}
@(private)
shade_build_lut :: proc(sh: ^Shading, lut: ^Shade_Lut) {
in_: [1]f64
for i in 0 ..< SHADE_LUT_SIZE {
s := f64(i) / f64(SHADE_LUT_SIZE - 1)
in_[0] = sh.t0 + s * (sh.t1 - sh.t0)
lut.rgb[i] = shade_eval(sh, in_[:])
}
}
// shade_lut_at looks a parametric position 0..1 up, rounding to the nearest
// sample so the error is half a step rather than a whole one.
@(private)
shade_lut_at :: proc(lut: ^Shade_Lut, s: f64) -> [3]f64 {
if !(s > 0) {
return lut.rgb[0]
}
if s >= 1 {
return lut.rgb[SHADE_LUT_SIZE - 1]
}
i := int(s * f64(SHADE_LUT_SIZE - 1) + 0.5)
return lut.rgb[clamp(i, 0, SHADE_LUT_SIZE - 1)]
}
// ---------------------------------------------------------------------------
// Bounds
// ---------------------------------------------------------------------------
// shading_bounds is the device-space region the shading can paint under `ctm`.
//
// RECT_INFINITE means "unbounded", which is the honest answer for an axial
// shading (the strip between its two ends is infinite across the axis, and an
// /Extend makes it infinite along it too) and for an extended radial one. It
// is the identity for `rect_intersect`, so a caller that intersects it with a
// clip gets the clip, which is exactly right.
shading_bounds :: proc(sh: ^Shading, ctm: Matrix) -> Rect {
if sh == nil || !matrix_is_finite(ctm) {
return RECT_EMPTY
}
r := RECT_INFINITE
switch sh.kind {
case .Function_Based:
dom := Rect{sh.domain[0], sh.domain[2], sh.domain[1], sh.domain[3]}
r = transform_rect(matrix_mul(sh.mtx, ctm), dom)
case .Axial:
// Unbounded across the axis whatever /Extend says.
r = RECT_INFINITE
case .Radial:
if !sh.extend[0] && !sh.extend[1] {
// Every circle between the two ends is a convex combination of a
// point of the first disc and a point of the second, so the two
// discs bound the whole family.
//
// The corners are taken with min and max rather than by unioning
// two rectangles: a circle of radius zero -- which is how every
// "glow" gradient starts -- is an empty rectangle, and
// `rect_union` drops an empty operand, which would shrink the
// bound to the other circle and clip the narrow end of the cone
// away.
rr0 := max(sh.coords[2], 0)
rr1 := max(sh.coords[5], 0)
r = transform_rect(ctm, Rect{
min(sh.coords[0] - rr0, sh.coords[3] - rr1),
min(sh.coords[1] - rr0, sh.coords[4] - rr1),
max(sh.coords[0] + rr0, sh.coords[3] + rr1),
max(sh.coords[1] + rr0, sh.coords[4] + rr1),
})
}
case .Free_Triangle, .Lattice_Triangle, .Coons, .Tensor:
// /Decode fixes the range every coordinate in the stream decodes into,
// so the mesh is bounded without reading a byte of it.
if len(sh.decode) >= 4 {
r = transform_rect(ctm, rect_normalize(
Rect{sh.decode[0], sh.decode[2], sh.decode[1], sh.decode[3]}))
}
}
if sh.has_bbox {
r = rect_intersect(r, transform_rect(ctm, sh.bbox))
}
if !rect_is_finite(r) {
return RECT_INFINITE
}
return r
}
// ---------------------------------------------------------------------------
// Painting
// ---------------------------------------------------------------------------
@(private)
Shade_Ctx :: struct {
doc: ^Document,
sh: ^Shading,
bmp: ^Bitmap,
clip: Raster_Clip,
alpha: f64,
mode: Blend_Mode,
mask: ^Bitmap,
ctm: Matrix,
// Device space back to the shading's own space.
inv: Matrix,
// One scratch row of straight RGBA8, indexed from clip.x0. Alpha 0 marks a
// pixel the shading does not cover.
row: []u8,
// The mask's coverage for `mask_row_y`, one byte per pixel from clip.x0,
// or nil when there is no mask.
mask_row: []u8,
mask_row_y: int,
fuel: int,
tris: int,
}
// shading_paint composites `sh` onto `bmp` through `clip`.
//
// ctm maps the shading's own space to device space
// clip device pixels, half-open, intersected with the bitmap here
// alpha the constant alpha of the paint operation
// mask an optional soft mask aligned with `bmp` in device pixels, one
// component read (what `mask_from_bitmap` produces). Pixels outside
// it read as 255, so a mask smaller than the clip shows content
// rather than erasing it.
//
// `bmp` is PREMULTIPLIED, like everything below the Device line. A singular or
// non-finite CTM paints nothing, and so does a colour space of /None.
// `context.allocator` supplies scratch only; everything it takes is released
// before this returns.
shading_paint :: proc(doc: ^Document, sh: ^Shading, bmp: ^Bitmap, ctm: Matrix,
clip: Raster_Clip, alpha: f64, mode: Blend_Mode, mask: ^Bitmap) -> Error {
if sh == nil || bmp == nil || sh.cs == nil {
return .Invalid_Argument
}
// A /None separation marks nothing at all on the page (8.6.6.4).
if sh.cs.is_none {
return .None
}
if !matrix_is_finite(ctm) {
return .None
}
inv, invertible := matrix_invert(ctm)
if !invertible {
return .None
}
ga := blend_clamp01(alpha)
if ga <= 0 {
return .None
}
cl := Raster_Clip{
x0 = max(clip.x0, 0),
y0 = max(clip.y0, 0),
x1 = min(clip.x1, bmp.width),
y1 = min(clip.y1, bmp.height),
}
if cl.x1 <= cl.x0 || cl.y1 <= cl.y0 {
return .None
}
// Narrowing to what the shading can possibly reach keeps a small gradient
// on a big page from walking the whole page, and is what applies /BBox to
// a mesh.
b := shading_bounds(sh, ctm)
cl = Raster_Clip{
x0 = raster_floor_clamped(b.x0, cl.x0, cl.x1),
y0 = raster_floor_clamped(b.y0, cl.y0, cl.y1),
x1 = raster_ceil_clamped(b.x1, cl.x0, cl.x1),
y1 = raster_ceil_clamped(b.y1, cl.y0, cl.y1),
}
if cl.x1 <= cl.x0 || cl.y1 <= cl.y0 {
return .None
}
width := cl.x1 - cl.x0
row, row_err := make([]u8, width * 4, context.allocator)
if row_err != nil {
return .Out_Of_Memory
}
defer delete(row, context.allocator)
ctx := Shade_Ctx{
doc = doc,
sh = sh,
bmp = bmp,
clip = cl,
alpha = ga,
mode = mode,
mask = mask,
ctm = ctm,
inv = inv,
row = row,
mask_row_y = -1,
fuel = SHADE_PIXEL_FUEL,
}
if mask != nil {
mrow, mask_err := make([]u8, width, context.allocator)
if mask_err != nil {
return .Out_Of_Memory
}
ctx.mask_row = mrow
}
// Freed here rather than in the branch above: a `defer` runs at the end of
// the block it sits in, so one inside the `if` would free the row before a
// single span had been composited through it.
defer if ctx.mask_row != nil { delete(ctx.mask_row, context.allocator) }
switch sh.kind {
case .Function_Based:
shade_paint_function(&ctx)
case .Axial:
shade_paint_axial(&ctx)
case .Radial:
shade_paint_radial(&ctx)
case .Free_Triangle, .Lattice_Triangle, .Coons, .Tensor:
return shade_paint_mesh(&ctx)
}
return .None
}
// shade_put writes one covered pixel into the scratch row.
@(private)
shade_put :: proc(ctx: ^Shade_Ctx, x: int, rgb: [3]f64) {
i := (x - ctx.clip.x0) * 4
if i < 0 || i + 4 > len(ctx.row) {
return
}
ctx.row[i] = blend_quantize(rgb[0])
ctx.row[i + 1] = blend_quantize(rgb[1])
ctx.row[i + 2] = blend_quantize(rgb[2])
ctx.row[i + 3] = 255
}
@(private)
shade_skip :: proc(ctx: ^Shade_Ctx, x: int) {
i := (x - ctx.clip.x0) * 4
if i < 0 || i + 4 > len(ctx.row) {
return
}
ctx.row[i + 3] = 0
}
// shade_mask_row lifts the mask's coverage for scanline `y` into a byte per
// pixel, which is the form `blend_span_rgba8` indexes.
@(private)
shade_mask_row :: proc(ctx: ^Shade_Ctx, y: int) {
if ctx.mask_row == nil || ctx.mask_row_y == y {
return
}
ctx.mask_row_y = y
for i in 0 ..< len(ctx.mask_row) {
ctx.mask_row[i] = 255
}
if ctx.mask == nil {
return
}
src := bitmap_row(ctx.mask, y)
if src == nil {
return
}
n := ctx.mask.n
for i in 0 ..< len(ctx.mask_row) {
x := ctx.clip.x0 + i
if x >= ctx.mask.width {
break
}
off := x * n
if off < len(src) {
ctx.mask_row[i] = src[off]
}
}
}
// shade_flush composites the covered runs of `row` between x_from and x_to.
@(private)
shade_flush :: proc(ctx: ^Shade_Ctx, y, x_from, x_to: int) {
lo := max(x_from, ctx.clip.x0)
hi := min(x_to, ctx.clip.x1)
if lo >= hi {
return
}
shade_mask_row(ctx, y)
i := lo
for i < hi {
if ctx.row[(i - ctx.clip.x0) * 4 + 3] == 0 {
i += 1
continue
}
j := i
for j < hi && ctx.row[(j - ctx.clip.x0) * 4 + 3] != 0 {
j += 1
}
a := (i - ctx.clip.x0) * 4
b := (j - ctx.clip.x0) * 4
mrow: []u8
if ctx.mask_row != nil {
mrow = ctx.mask_row[i - ctx.clip.x0 : j - ctx.clip.x0]
}
blend_span_rgba8(ctx.bmp, y, i, ctx.row[a:b], ctx.alpha, ctx.mode, nil, mrow)
i = j
}
}
// shade_in_bbox tests a device pixel centre against /BBox, which is given in
// the shading's own space. Testing there rather than against the transformed
// box keeps a rotated CTM from painting the corners the box excludes.
@(private)
shade_in_bbox :: proc(ctx: ^Shade_Ctx, p: Point) -> bool {
if !ctx.sh.has_bbox {
return true
}
return rect_contains(ctx.sh.bbox, p)
}
// ---------------------------------------------------------------------------
// Type 1: function-based
// ---------------------------------------------------------------------------
@(private)
shade_paint_function :: proc(ctx: ^Shade_Ctx) {
sh := ctx.sh
// /Matrix maps the domain into the shading's space, which the CTM then
// maps to the device; one inverse takes a pixel all the way back.
full, ok := matrix_invert(matrix_mul(sh.mtx, ctx.ctm))
if !ok {
return
}
in_: [2]f64
for y in ctx.clip.y0 ..< ctx.clip.y1 {
for x in ctx.clip.x0 ..< ctx.clip.x1 {
dev := Point{f64(x) + 0.5, f64(y) + 0.5}
p := transform_point(full, dev)
if !is_finite(p.x) || !is_finite(p.y) ||
p.x < sh.domain[0] || p.x > sh.domain[1] ||
p.y < sh.domain[2] || p.y > sh.domain[3] {
shade_skip(ctx, x)
continue
}
if sh.has_bbox && !shade_in_bbox(ctx, transform_point(ctx.inv, dev)) {
shade_skip(ctx, x)
continue
}
in_[0] = p.x
in_[1] = p.y
shade_put(ctx, x, shade_eval(sh, in_[:]))
}
shade_flush(ctx, y, ctx.clip.x0, ctx.clip.x1)
ctx.fuel -= ctx.clip.x1 - ctx.clip.x0
if ctx.fuel <= 0 {
return
}
}
}
// ---------------------------------------------------------------------------
// Type 2: axial
// ---------------------------------------------------------------------------
@(private)
shade_paint_axial :: proc(ctx: ^Shade_Ctx) {
sh := ctx.sh
p0 := Point{sh.coords[0], sh.coords[1]}
d := Point{sh.coords[2] - p0.x, sh.coords[3] - p0.y}
len2 := d.x * d.x + d.y * d.y
// A zero-length axis leaves the parametric variable undefined; there is no
// colour to choose, so nothing is painted.
if !(len2 > 0) {
return
}
inv_len2 := 1 / len2
lut: Shade_Lut
shade_build_lut(sh, &lut)
for y in ctx.clip.y0 ..< ctx.clip.y1 {
for x in ctx.clip.x0 ..< ctx.clip.x1 {
p := transform_point(ctx.inv, Point{f64(x) + 0.5, f64(y) + 0.5})
s := ((p.x - p0.x) * d.x + (p.y - p0.y) * d.y) * inv_len2
if !is_finite(s) || !shade_in_bbox(ctx, p) {
shade_skip(ctx, x)
continue
}
if s < 0 {
if !sh.extend[0] {
shade_skip(ctx, x)
continue
}
s = 0
} else if s > 1 {
if !sh.extend[1] {
shade_skip(ctx, x)
continue
}
s = 1
}
shade_put(ctx, x, shade_lut_at(&lut, s))
}
shade_flush(ctx, y, ctx.clip.x0, ctx.clip.x1)
ctx.fuel -= ctx.clip.x1 - ctx.clip.x0
if ctx.fuel <= 0 {
return
}
}
}
// ---------------------------------------------------------------------------
// Type 3: radial
// ---------------------------------------------------------------------------
// shade_radial_accept tests one candidate s against the two rules that decide
// whether a circle in the family exists there at all: its radius must not be
// negative (8.7.4.5.4 -- the extension stops where the circles would invert),
// and a value outside 0..1 needs the matching /Extend. The returned value is
// clamped, which is what makes an extended shading paint its end colour.
@(private)
shade_radial_accept :: proc(s, r0, dr: f64, extend: [2]bool) -> (f64, bool) {
if !is_finite(s) {
return 0, false
}
if r0 + s * dr < 0 {
return 0, false
}
if s < 0 {
return 0, extend[0]
}
if s > 1 {
return 1, extend[1]
}
return s, true
}
// shade_radial_solve finds the largest s whose circle passes through the point,
// as the format requires: later circles paint over earlier ones, so the largest
// s is the one that shows.
//
// a = dx^2 + dy^2 - dr^2
// b = px*dx + py*dy + r0*dr
// c = px^2 + py^2 - r0^2
//
// solving a*s^2 - 2*b*s + c = 0. `a` is zero exactly when the circles' radii
// grow as fast as their centres move -- the cone degenerates to a paraboloid
// and the equation is linear. A negative discriminant means no circle in the
// family reaches the point, which must paint nothing rather than an end colour.
@(private)
shade_radial_solve :: proc(a, b, c, r0, dr: f64, extend: [2]bool) -> (f64, bool) {
if a == 0 {
if b == 0 {
return 0, false
}
return shade_radial_accept(c / (2 * b), r0, dr, extend)
}
disc := b * b - a * c
if !(disc >= 0) {
return 0, false
}
sq := math.sqrt(disc)
s1 := (b + sq) / a
s2 := (b - sq) / a
hi := max(s1, s2)
lo := min(s1, s2)
if s, ok := shade_radial_accept(hi, r0, dr, extend); ok {
return s, true
}
return shade_radial_accept(lo, r0, dr, extend)
}
@(private)
shade_paint_radial :: proc(ctx: ^Shade_Ctx) {
sh := ctx.sh
x0, y0, r0 := sh.coords[0], sh.coords[1], sh.coords[2]
dx := sh.coords[3] - x0
dy := sh.coords[4] - y0
dr := sh.coords[5] - r0
a := dx * dx + dy * dy - dr * dr
lut: Shade_Lut
shade_build_lut(sh, &lut)
for y in ctx.clip.y0 ..< ctx.clip.y1 {
for x in ctx.clip.x0 ..< ctx.clip.x1 {
p := transform_point(ctx.inv, Point{f64(x) + 0.5, f64(y) + 0.5})
if !is_finite(p.x) || !is_finite(p.y) || !shade_in_bbox(ctx, p) {
shade_skip(ctx, x)
continue
}
px := p.x - x0
py := p.y - y0
b := px * dx + py * dy + r0 * dr
c := px * px + py * py - r0 * r0
s, ok := shade_radial_solve(a, b, c, r0, dr, sh.extend)
if !ok {
shade_skip(ctx, x)
continue
}
shade_put(ctx, x, shade_lut_at(&lut, s))
}
shade_flush(ctx, y, ctx.clip.x0, ctx.clip.x1)
ctx.fuel -= ctx.clip.x1 - ctx.clip.x0
if ctx.fuel <= 0 {
return
}
}
}
// ---------------------------------------------------------------------------
// Triangles
// ---------------------------------------------------------------------------
@(private)
Shade_Vertex :: struct {
// Device space: the mesh is transformed as it is read, so the whole
// rasteriser works in one space.
p: Point,
rgb: [3]f64,
}
// shade_top_left reports whether an edge owns the pixel centres that land
// exactly on it. With triangles wound to a positive area, the interior lies on
// the e >= 0 side, which makes an edge running upwards a left edge and a
// horizontal edge running right a top edge. Two triangles sharing an edge
// traverse it in opposite directions, so exactly one of them owns it.
@(private)
shade_top_left :: proc "contextless" (d: Point) -> bool {
return d.y < 0 || (d.y == 0 && d.x > 0)
}
@(private)
shade_edge_accept :: proc "contextless" (w: f64, top_left: bool) -> bool {
// Written so a NaN is outside rather than inside.
return w > 0 || (w == 0 && top_left)
}
// shade_triangle scan-converts one Gouraud-shaded triangle at pixel centres.
@(private)
shade_triangle :: proc(ctx: ^Shade_Ctx, va, vb, vc: Shade_Vertex) {
a := va
b := vb
c := vc
if !is_finite(a.p.x) || !is_finite(a.p.y) ||
!is_finite(b.p.x) || !is_finite(b.p.y) ||
!is_finite(c.p.x) || !is_finite(c.p.y) {
return
}
area := (b.p.x - a.p.x) * (c.p.y - a.p.y) - (b.p.y - a.p.y) * (c.p.x - a.p.x)
if !is_finite(area) || area == 0 {
return
}
if area < 0 {
// Winding every triangle the same way is what makes the top-left rule
// consistent between neighbours: an adjacent triangle on the far side
// of a shared edge necessarily traverses that edge the other way.
b, c = c, b
area = -area
}
ctx.tris += 1
bx0 := raster_floor_clamped(min(a.p.x, b.p.x, c.p.x), ctx.clip.x0, ctx.clip.x1)
bx1 := raster_ceil_clamped(max(a.p.x, b.p.x, c.p.x), ctx.clip.x0, ctx.clip.x1)
by0 := raster_floor_clamped(min(a.p.y, b.p.y, c.p.y), ctx.clip.y0, ctx.clip.y1)
by1 := raster_ceil_clamped(max(a.p.y, b.p.y, c.p.y), ctx.clip.y0, ctx.clip.y1)
if bx1 <= bx0 || by1 <= by0 {
return
}
d0 := Point{b.p.x - a.p.x, b.p.y - a.p.y}
d1 := Point{c.p.x - b.p.x, c.p.y - b.p.y}
d2 := Point{a.p.x - c.p.x, a.p.y - c.p.y}
tl0 := shade_top_left(d0)
tl1 := shade_top_left(d1)
tl2 := shade_top_left(d2)
inv_area := 1 / area
for y in by0 ..< by1 {
py := f64(y) + 0.5
for x in bx0 ..< bx1 {
px := f64(x) + 0.5
// Recomputed rather than stepped along the row on purpose: the
// same edge evaluated from two neighbouring triangles must give
// bit-identical results, and an incremental sum would depend on
// where each triangle's bounding box started.
w0 := d0.x * (py - a.p.y) - d0.y * (px - a.p.x)
w1 := d1.x * (py - b.p.y) - d1.y * (px - b.p.x)
w2 := d2.x * (py - c.p.y) - d2.y * (px - c.p.x)
if !shade_edge_accept(w0, tl0) || !shade_edge_accept(w1, tl1) ||
!shade_edge_accept(w2, tl2) {
shade_skip(ctx, x)
continue
}
if ctx.sh.has_bbox && !shade_in_bbox(ctx, transform_point(ctx.inv, Point{px, py})) {
shade_skip(ctx, x)
continue
}
// w1 is the edge opposite a, w2 opposite b, w0 opposite c.
la := w1 * inv_area
lb := w2 * inv_area
lc := w0 * inv_area
rgb := [3]f64{
la * a.rgb[0] + lb * b.rgb[0] + lc * c.rgb[0],
la * a.rgb[1] + lb * b.rgb[1] + lc * c.rgb[1],
la * a.rgb[2] + lb * b.rgb[2] + lc * c.rgb[2],
}
shade_put(ctx, x, rgb)
}
shade_flush(ctx, y, bx0, bx1)
ctx.fuel -= bx1 - bx0
if ctx.fuel <= 0 {
return
}
}
}
// ---------------------------------------------------------------------------
// Mesh streams
// ---------------------------------------------------------------------------
@(private)
Mesh_Reader :: struct {
sh: ^Shading,
ctm: Matrix,
data: []byte,
// Bit position, MSB first, which is how every packed field in a PDF is
// written.
pos: int,
max_coord: f64,
max_comp: f64,
count: int,
}
@(private)
mesh_reader_make :: proc(sh: ^Shading, ctm: Matrix, data: []byte) -> Mesh_Reader {
return Mesh_Reader{
sh = sh,
ctm = ctm,
data = data,
max_coord = math.pow(2, f64(sh.bits_per_coord)) - 1,
max_comp = math.pow(2, f64(sh.bits_per_component)) - 1,
}
}
// mesh_bits reads the next `n` bits. Returns ok = false at end of data, and
// leaves the reader at the end so every later read fails too -- a truncated
// mesh stops, it does not wrap around and paint garbage.
@(private)
mesh_bits :: proc(r: ^Mesh_Reader, n: int) -> (v: u32, ok: bool) {
total := len(r.data) * 8
if n <= 0 || n > 32 || r.pos + n > total {
r.pos = total
return 0, false
}
acc: u64
left := n
for left > 0 {
byte_index := r.pos >> 3
bit_off := r.pos & 7
avail := 8 - bit_off
take := min(avail, left)
chunk := (u64(r.data[byte_index]) >> uint(avail - take)) & u64((1 << uint(take)) - 1)
acc = acc << uint(take) | chunk
r.pos += take
left -= take
}
return u32(acc), true
}
// mesh_align skips to the next byte boundary. Free-form triangles and patches
// pad each record to a whole number of bytes; lattice rows do not.
@(private)
mesh_align :: proc(r: ^Mesh_Reader) {
r.pos = ((r.pos + 7) / 8) * 8
}
@(private)
mesh_decode :: proc(v: u32, maxv, lo, hi: f64) -> f64 {
if !(maxv > 0) {
return lo
}
return lo + f64(v) * (hi - lo) / maxv
}
@(private)
mesh_point :: proc(r: ^Mesh_Reader) -> (Point, bool) {
xv, ok0 := mesh_bits(r, r.sh.bits_per_coord)
yv, ok1 := mesh_bits(r, r.sh.bits_per_coord)
if !ok0 || !ok1 {
return Point{}, false
}
dec := r.sh.decode
p := Point{
mesh_decode(xv, r.max_coord, dec[0], dec[1]),
mesh_decode(yv, r.max_coord, dec[2], dec[3]),
}
return transform_point(r.ctm, p), true
}
@(private)
mesh_color :: proc(r: ^Mesh_Reader) -> ([3]f64, bool) {
comps: [MAX_COLOR_COMPONENTS]f64
sh := r.sh
dec := sh.decode
for i in 0 ..< sh.n_comp {
cv, ok := mesh_bits(r, sh.bits_per_component)
if !ok {
return [3]f64{}, false
}
comps[i] = mesh_decode(cv, r.max_comp, dec[4 + 2 * i], dec[5 + 2 * i])
}
if len(sh.funcs) > 0 {
return shade_eval(sh, comps[:1]), true
}
return shade_components_to_rgb(sh, comps[:sh.n_comp]), true
}
@(private)
mesh_vertex :: proc(r: ^Mesh_Reader, out: ^Shade_Vertex) -> bool {
if r.count >= MAX_MESH_VERTICES {
return false
}
p, p_ok := mesh_point(r)
if !p_ok {
return false
}
rgb, c_ok := mesh_color(r)
if !c_ok {
return false
}
out.p = p
out.rgb = rgb
r.count += 1
return true
}
@(private)
shade_paint_mesh :: proc(ctx: ^Shade_Ctx) -> Error {
sh := ctx.sh
if sh.stream == nil || len(sh.decode) < 4 + 2 * sh.n_comp {
return .None
}
data, err := stream_data(ctx.doc, sh.stream)
if err != .None || len(data) == 0 {
return .None
}
r := mesh_reader_make(sh, ctx.ctm, data)
switch sh.kind {
case .Free_Triangle:
shade_mesh_free(ctx, &r)
case .Lattice_Triangle:
return shade_mesh_lattice(ctx, &r)
case .Coons, .Tensor:
shade_mesh_patches(ctx, &r)
case .Function_Based, .Axial, .Radial:
// Not reachable: shading_paint dispatches those elsewhere.
}
return .None
}
// ---------------------------------------------------------------------------
// Type 4: free-form triangles
// ---------------------------------------------------------------------------
@(private)
mesh_flagged_vertex :: proc(r: ^Mesh_Reader, out: ^Shade_Vertex) -> bool {
// The two vertices that follow a flag-0 vertex carry a flag of their own,
// which the format says shall be 0 and which nothing reads.
if _, ok := mesh_bits(r, r.sh.bits_per_flag); !ok {
return false
}
if !mesh_vertex(r, out) {
return false
}
mesh_align(r)
return true
}
@(private)
shade_mesh_free :: proc(ctx: ^Shade_Ctx, r: ^Mesh_Reader) {
va, vb, vc: Shade_Vertex
have := false
for ctx.tris < MAX_MESH_TRIANGLES && ctx.fuel > 0 {
flag, flag_ok := mesh_bits(r, r.sh.bits_per_flag)
if !flag_ok {
return
}
v: Shade_Vertex
if !mesh_vertex(r, &v) {
return
}
mesh_align(r)
switch {
case flag == 0 || !have:
// A continuation flag with no previous triangle is malformed;
// starting a fresh triangle is the recovery that loses least.
v1, v2: Shade_Vertex
if !mesh_flagged_vertex(r, &v1) || !mesh_flagged_vertex(r, &v2) {
return
}
va, vb, vc = v, v1, v2
case flag == 1:
va, vb, vc = vb, vc, v
case flag == 2:
va, vb, vc = va, vc, v
case:
add_repair(ctx.doc, .Bad_Resource, 0)
return
}
shade_triangle(ctx, va, vb, vc)
have = true
}
}
// ---------------------------------------------------------------------------
// Type 5: lattice-form triangles
// ---------------------------------------------------------------------------
@(private)
shade_mesh_lattice :: proc(ctx: ^Shade_Ctx, r: ^Mesh_Reader) -> Error {
per_row := ctx.sh.vertices_per_row
if per_row < 2 || per_row > MAX_MESH_ROW {
return .None
}
rows, rows_err := make([]Shade_Vertex, per_row * 2, context.allocator)
if rows_err != nil {
return .Out_Of_Memory
}
defer delete(rows, context.allocator)
prev := rows[:per_row]
cur := rows[per_row:]
have_prev := false
for ctx.tris < MAX_MESH_TRIANGLES && ctx.fuel > 0 {
complete := true
for i in 0 ..< per_row {
if !mesh_vertex(r, &cur[i]) {
complete = false
break
}
}
// A row that ran out mid-way is dropped whole: half a row has no
// triangles in it.
if !complete {
return .None
}
if have_prev {
for i in 0 ..< per_row - 1 {
shade_triangle(ctx, prev[i], prev[i + 1], cur[i])
shade_triangle(ctx, prev[i + 1], cur[i + 1], cur[i])
}
}
prev, cur = cur, prev
have_prev = true
}
return .None
}
// ---------------------------------------------------------------------------
// Types 6 and 7: Coons and tensor patches
// ---------------------------------------------------------------------------
// The twelve boundary control points are written anticlockwise from one
// corner, which lays them onto the 4x4 tensor grid like this (ISO 32000-2,
// figures 46 and 47):
//
// p1 p2 p3 p4 [0][0] [0][1] [0][2] [0][3]
// p12 p5 [1][0] p13 p14 [1][3]
// p11 p6 [2][0] p16 p15 [2][3]
// p10 p9 p8 p7 [3][0] [3][1] [3][2] [3][3]
//
// with the corner colours c1..c4 at p1, p4, p7 and p10 in that order.
@(private)
PATCH_GRID := [12][2]int{
{0, 0}, {0, 1}, {0, 2}, {0, 3},
{1, 3}, {2, 3}, {3, 3},
{3, 2}, {3, 1}, {3, 0},
{2, 0}, {1, 0},
}
// PATCH_SHARED tells which of the previous patch's points and colours a flag
// of 1, 2 or 3 reuses as the new patch's first edge (Table 85).
@(private)
PATCH_SHARED := [4][4]int{
{0, 0, 0, 0}, // flag 0: nothing is shared
{3, 4, 5, 6},
{6, 7, 8, 9},
{9, 10, 11, 0},
}
@(private)
PATCH_SHARED_COLOR := [4][2]int{
{0, 0},
{1, 2},
{2, 3},
{3, 0},
}
@(private)
shade_mesh_patches :: proc(ctx: ^Shade_Ctx, r: ^Mesh_Reader) {
n_points := ctx.sh.kind == .Tensor ? 16 : 12
pts, prev_pts: [16]Point
cols, prev_cols: [4][3]f64
have := false
for _ in 0 ..< MAX_MESH_PATCHES {
if ctx.tris >= MAX_MESH_TRIANGLES || ctx.fuel <= 0 {
return
}
raw_flag, flag_ok := mesh_bits(r, r.sh.bits_per_flag)
if !flag_ok {
return
}
flag := int(raw_flag)
if flag > 3 {
add_repair(ctx.doc, .Bad_Resource, 0)
return
}
first_point := 0
first_color := 0
if flag != 0 && have {
share := PATCH_SHARED[flag]
for k in 0 ..< 4 {
pts[k] = prev_pts[share[k]]
}
cshare := PATCH_SHARED_COLOR[flag]
cols[0] = prev_cols[cshare[0]]
cols[1] = prev_cols[cshare[1]]
first_point = 4
first_color = 2
}
for k in first_point ..< n_points {
p, ok := mesh_point(r)
if !ok {
return
}
pts[k] = p
}
for k in first_color ..< 4 {
c, ok := mesh_color(r)
if !ok {
return
}
cols[k] = c
}
mesh_align(r)
shade_patch(ctx, pts, cols, ctx.sh.kind == .Tensor)
prev_pts = pts
prev_cols = cols
have = true
}
}
@(private)
bernstein3 :: proc "contextless" (t: f64) -> [4]f64 {
mt := 1 - t
return [4]f64{mt * mt * mt, 3 * mt * mt * t, 3 * mt * t * t, t * t * t}
}
@(private)
patch_eval :: proc(pole: ^[4][4]Point, u, v: f64) -> (p: Point) {
bu := bernstein3(u)
bv := bernstein3(v)
for i in 0 ..< 4 {
for j in 0 ..< 4 {
w := bu[i] * bv[j]
p.x += w * pole[i][j].x
p.y += w * pole[i][j].y
}
}
return
}
// patch_color is the bilinear interpolation of the four corner colours the
// format prescribes (8.7.4.5.7): c1 at (0,0), c2 at (0,1), c3 at (1,1) and
// c4 at (1,0), matching the corners the control points put there.
@(private)
patch_color :: proc(c: [4][3]f64, u, v: f64) -> (out: [3]f64) {
for i in 0 ..< 3 {
e0 := (1 - v) * c[0][i] + v * c[1][i]
e1 := (1 - v) * c[3][i] + v * c[2][i]
out[i] = (1 - u) * e0 + u * e1
}
return
}
// shade_patch subdivides one patch into triangles.
//
// The subdivision is uniform, with its step chosen from the patch's device
// size so that a step is about PATCH_STEP_PIXELS across: a patch a few pixels
// wide costs two triangles, and one covering a page is split as far as the cap
// allows. Uniform subdivision rather than adaptive flatness testing is a
// deliberate trade -- a Bezier patch's colour varies over its whole area, not
// just where it curves, so the flat parts need the same subdivision as the
// curved ones and adaptivity would buy nothing.
@(private)
shade_patch :: proc(ctx: ^Shade_Ctx, v: [16]Point, c: [4][3]f64, tensor: bool) {
pole: [4][4]Point
for k in 0 ..< 12 {
g := PATCH_GRID[k]
pole[g[0]][g[1]] = v[k]
}
if tensor {
pole[1][1] = v[12]
pole[1][2] = v[13]
pole[2][2] = v[14]
pole[2][1] = v[15]
} else {
// A Coons patch gives no interior control points; the format defines
// them from the boundary (8.7.4.5.7).
pole[1][1] = shade_coons_inner(pole[0][0], pole[0][1], pole[1][0], pole[0][3], pole[3][0], pole[3][1], pole[1][3], pole[3][3])
pole[1][2] = shade_coons_inner(pole[0][3], pole[0][2], pole[1][3], pole[0][0], pole[3][3], pole[3][2], pole[1][0], pole[3][0])
pole[2][1] = shade_coons_inner(pole[3][0], pole[3][1], pole[2][0], pole[3][3], pole[0][0], pole[0][1], pole[2][3], pole[0][3])
pole[2][2] = shade_coons_inner(pole[3][3], pole[3][2], pole[2][3], pole[3][0], pole[0][3], pole[0][2], pole[2][0], pole[0][0])
}
lo := pole[0][0]
hi := pole[0][0]
for i in 0 ..< 4 {
for j in 0 ..< 4 {
p := pole[i][j]
if !is_finite(p.x) || !is_finite(p.y) {
return
}
lo.x = min(lo.x, p.x); lo.y = min(lo.y, p.y)
hi.x = max(hi.x, p.x); hi.y = max(hi.y, p.y)
}
}
extent := max(hi.x - lo.x, hi.y - lo.y)
n := 1
if is_finite(extent) && extent > 0 {
steps := math.ceil(extent / PATCH_STEP_PIXELS)
if steps >= f64(MAX_PATCH_SUBDIV) {
n = MAX_PATCH_SUBDIV
} else if steps > 1 {
n = int(steps)
}
}
inv_n := 1 / f64(n)
row_a, row_b: [MAX_PATCH_SUBDIV + 1]Point
for j in 0 ..= n {
row_a[j] = patch_eval(&pole, 0, f64(j) * inv_n)
}
for i in 1 ..= n {
u1 := f64(i) * inv_n
u0 := f64(i - 1) * inv_n
for j in 0 ..= n {
row_b[j] = patch_eval(&pole, u1, f64(j) * inv_n)
}
for j in 0 ..< n {
v0 := f64(j) * inv_n
v1 := f64(j + 1) * inv_n
p00 := Shade_Vertex{row_a[j], patch_color(c, u0, v0)}
p01 := Shade_Vertex{row_a[j + 1], patch_color(c, u0, v1)}
p11 := Shade_Vertex{row_b[j + 1], patch_color(c, u1, v1)}
p10 := Shade_Vertex{row_b[j], patch_color(c, u1, v0)}
shade_triangle(ctx, p00, p01, p11)
shade_triangle(ctx, p00, p11, p10)
}
row_a = row_b
if ctx.fuel <= 0 || ctx.tris >= MAX_MESH_TRIANGLES {
return
}
}
}
// shade_coons_inner is the interior control point the format derives from the
// boundary of a Coons patch:
//
// p11 = (-4*p00 + 6*(p01 + p10) - 2*(p03 + p30) + 3*(p31 + p13) - p33) / 9
//
// with the arguments named for the corner it belongs to; the other three come
// from the same expression with the grid rotated.
@(private)
shade_coons_inner :: proc "contextless" (corner, along_a, along_b, far_a, far_b, near_a, near_b, opposite: Point) -> Point {
return Point{
(-4 * corner.x + 6 * (along_a.x + along_b.x) - 2 * (far_a.x + far_b.x) +
3 * (near_a.x + near_b.x) - opposite.x) / 9,
(-4 * corner.y + 6 * (along_a.y + along_b.y) - 2 * (far_a.y + far_b.y) +
3 * (near_a.y + near_b.y) - opposite.y) / 9,
}
}