Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions crates/n0/src/drawlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,20 @@ pub(crate) enum ResolvedFilterMorphology {
Dilate,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ResolvedFilterTurbulenceKind {
Turbulence,
FractalNoise,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ResolvedFilterDisplacementChannel {
Red,
Green,
Blue,
Alpha,
}

/// The private filter-operation vocabulary admitted by the painter.
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum ResolvedFilterPrimitive {
Expand Down Expand Up @@ -144,6 +158,19 @@ pub(crate) enum ResolvedFilterPrimitive {
radius_x: f32,
radius_y: f32,
},
Turbulence {
kind: ResolvedFilterTurbulenceKind,
base_frequency_x: f32,
base_frequency_y: f32,
num_octaves: u8,
seed: f32,
stitch_tiles: bool,
},
DisplacementMap {
scale: f32,
x_channel: ResolvedFilterDisplacementChannel,
y_channel: ResolvedFilterDisplacementChannel,
},
Merge,
}

Expand All @@ -160,6 +187,14 @@ pub(crate) struct ResolvedFilterNode {
pub struct ResolvedFilter {
pub(crate) region: n0_model::math::RectF,
pub(crate) nodes: Arc<[ResolvedFilterNode]>,
/// Whether the graph can create output from a fully transparent source.
/// Such a graph needs one explicit transparent source raster even when
/// its scope contains no draw item, or a lazy layer restore is skipped.
pub(crate) may_paint_transparent_input: bool,
/// The invocation's isolated source is known to be fully transparent.
/// Source references are materialized explicitly so generated/additive
/// graph output is not skipped by a lazy backend layer.
pub(crate) source_is_transparent: bool,
}

/// Product-local owner slot for a source-neutral glyphless frame.
Expand Down
142 changes: 127 additions & 15 deletions crates/n0/src/glyphless.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,18 @@ use n0_model::model::{
};
use n0_model::path::ResolvedPathArtifact;
use rframe::{
ClipPath, FilterBlend, FilterColorSpace, FilterComposite, FilterInput, FilterMorphology,
FilterPrimitive, FilterProgram, Frame, FrameItem, Geometry, MaskMode, PaintStack, ScopeEffect,
VisualRef,
ClipPath, FilterBlend, FilterColorSpace, FilterComposite, FilterDisplacementChannel,
FilterInput, FilterMorphology, FilterPrimitive, FilterTurbulenceKind, Frame, FrameItem,
Geometry, MaskMode, PaintStack, ScopeEffect, VisualRef,
};

use crate::damage::{diff_inputs, DamageOwner, FrameDamageInput};
use crate::drawlist::{
DrawList, GlyphlessOwnerSlot, Item, ItemKind, PostPaintOpacity, ResolvedClipGeometry,
ResolvedClipGeometryKind, ResolvedClipLayer, ResolvedClipPath, ResolvedFilter,
ResolvedFilterBlend, ResolvedFilterColorSpace, ResolvedFilterComposite, ResolvedFilterInput,
ResolvedFilterMorphology, ResolvedFilterNode, ResolvedFilterPrimitive, ResolvedMaskMode,
ResolvedFilterBlend, ResolvedFilterColorSpace, ResolvedFilterComposite,
ResolvedFilterDisplacementChannel, ResolvedFilterInput, ResolvedFilterMorphology,
ResolvedFilterNode, ResolvedFilterPrimitive, ResolvedFilterTurbulenceKind, ResolvedMaskMode,
StrokeDashPhase,
};
use crate::frame::FrameExecutionError;
Expand Down Expand Up @@ -356,7 +357,7 @@ pub fn compile(resolved: Frame) -> Result<FrameProduct, BuildError> {
provenance.owners.push(scope.owner);
// Placeholder until the scope closes and its union is known.
provenance.coverage.push(None);
let kind = match &scope.effect {
let (kind, initial_coverage) = match &scope.effect {
ScopeEffect::Opacity(opacity) => {
items.push(Item {
node: slot,
Expand All @@ -365,7 +366,7 @@ pub fn compile(resolved: Frame) -> Result<FrameProduct, BuildError> {
opacity: opacity.get(),
},
});
OpenScopeKind::Opacity
(OpenScopeKind::Opacity, None)
}
ScopeEffect::Clip(clip) => {
let compiled = Arc::new(compile_clip_path(clip));
Expand All @@ -383,10 +384,10 @@ pub fn compile(resolved: Frame) -> Result<FrameProduct, BuildError> {
world: Affine::IDENTITY,
kind: ItemKind::BeginClipPath { clip: compiled },
});
OpenScopeKind::Clip { bounds }
(OpenScopeKind::Clip { bounds }, None)
}
ScopeEffect::Filter(filter) => {
let compiled = Arc::new(compile_filter(filter.program(), filter.region()));
let compiled = Arc::new(compile_filter(filter));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if let Err(reason) = crate::paint::preflight_filter(&compiled) {
return Err(BuildError::Filter {
owner: scope.owner,
Expand All @@ -402,12 +403,19 @@ pub fn compile(resolved: Frame) -> Result<FrameProduct, BuildError> {
world: to_affine(filter.transform()),
kind: ItemKind::BeginFilter { filter: compiled },
});
OpenScopeKind::Filter { bounds }
let initial_coverage = if filter.source_is_transparent()
&& filter.program().may_paint_transparent_input()
{
bounds
} else {
None
};
(OpenScopeKind::Filter { bounds }, initial_coverage)
}
};
open_scopes.push(OpenScope {
slot,
coverage: None,
coverage: initial_coverage,
kind,
});
continue;
Expand Down Expand Up @@ -871,7 +879,8 @@ fn compile_clip_path(clip: &ClipPath) -> ResolvedClipPath {
/// Project one checked, source-neutral filter program into private painter
/// material. Every index and scalar has already been validated by `rframe`;
/// this is a vocabulary translation, not a second resolver.
fn compile_filter(program: &FilterProgram, region: math2::Rectangle) -> ResolvedFilter {
fn compile_filter(filter: &rframe::Filter) -> ResolvedFilter {
let program = filter.program();
let nodes = program
.iter()
.map(|node| ResolvedFilterNode {
Expand Down Expand Up @@ -969,14 +978,65 @@ fn compile_filter(program: &FilterProgram, region: math2::Rectangle) -> Resolved
radius_x,
radius_y,
},
FilterPrimitive::Turbulence {
kind,
base_frequency_x,
base_frequency_y,
num_octaves,
seed,
stitch_tiles,
} => ResolvedFilterPrimitive::Turbulence {
kind: match kind {
FilterTurbulenceKind::Turbulence => {
ResolvedFilterTurbulenceKind::Turbulence
}
FilterTurbulenceKind::FractalNoise => {
ResolvedFilterTurbulenceKind::FractalNoise
}
},
base_frequency_x,
base_frequency_y,
num_octaves,
seed,
stitch_tiles,
},
FilterPrimitive::DisplacementMap {
scale,
x_channel,
y_channel,
} => ResolvedFilterPrimitive::DisplacementMap {
scale,
x_channel: match x_channel {
FilterDisplacementChannel::Red => ResolvedFilterDisplacementChannel::Red,
FilterDisplacementChannel::Green => {
ResolvedFilterDisplacementChannel::Green
}
FilterDisplacementChannel::Blue => ResolvedFilterDisplacementChannel::Blue,
FilterDisplacementChannel::Alpha => {
ResolvedFilterDisplacementChannel::Alpha
}
},
y_channel: match y_channel {
FilterDisplacementChannel::Red => ResolvedFilterDisplacementChannel::Red,
FilterDisplacementChannel::Green => {
ResolvedFilterDisplacementChannel::Green
}
FilterDisplacementChannel::Blue => ResolvedFilterDisplacementChannel::Blue,
FilterDisplacementChannel::Alpha => {
ResolvedFilterDisplacementChannel::Alpha
}
},
},
FilterPrimitive::Merge => ResolvedFilterPrimitive::Merge,
},
})
.collect::<Vec<_>>()
.into();
ResolvedFilter {
region: to_rectf(region),
region: to_rectf(filter.region()),
nodes,
may_paint_transparent_input: program.may_paint_transparent_input(),
source_is_transparent: filter.source_is_transparent(),
}
}

Expand Down Expand Up @@ -1540,8 +1600,8 @@ mod tests {
};
use n0_model::resolve::ResolveOptions;
use rframe::{
Filter, FilterNode, FrameItems, FrameNode, Identity, PaintAlphaFactor, Provenance, Scope,
ScopeOpacity,
Filter, FilterNode, FilterProgram, FrameItems, FrameNode, Identity, PaintAlphaFactor,
Provenance, Scope, ScopeOpacity,
};
use skia_safe::surfaces;

Expand Down Expand Up @@ -1805,6 +1865,33 @@ mod tests {
})
}

fn empty_turbulence_scope_begin(owner: VisualRef, seed: f32) -> FrameItem {
let region = Rectangle::from_xywh(4.0, 5.0, 10.0, 12.0);
let program = FilterProgram::new(Arc::from([FilterNode::new(
Arc::from([]),
region,
FilterColorSpace::LinearRgb,
FilterPrimitive::Turbulence {
kind: FilterTurbulenceKind::Turbulence,
base_frequency_x: 0.08,
base_frequency_y: 0.11,
num_octaves: 2,
seed,
stitch_tiles: false,
},
)]))
.expect("test turbulence is a checked generated program");
let transform = AffineTransform::from_acebdf(1.0, 0.0, 7.0, 0.0, 1.0, 8.0);
FrameItem::ScopeBegin(Scope {
owner,
effect: ScopeEffect::Filter(
Filter::new(transform, region, program)
.expect("test filter is a checked effect")
.with_transparent_source(),
),
})
}

fn clip_begin(owner: VisualRef, layers: Vec<Vec<(Rectangle, AffineTransform)>>) -> FrameItem {
let layers = layers
.into_iter()
Expand Down Expand Up @@ -3057,6 +3144,31 @@ mod tests {
);
}

#[test]
fn an_empty_generated_filter_edit_damages_its_transformed_region() {
let scene = |seed| {
let items = FrameItems::try_new(vec![
empty_turbulence_scope_begin(SCOPE_OWNER, seed),
FrameItem::ScopeEnd,
])
.expect("a declared generated filter is meaningful without source draws");
compile(frame_of(items)).expect("admitted generated-filter frame")
};
let before = scene(2.0);
let after = scene(3.0);
let coverage = Rectangle::from_xywh(11.0, 13.0, 10.0, 12.0);

assert_eq!(coverage_for(&before, SCOPE_OWNER), Some(to_rectf(coverage)));
assert_eq!(
diff_frame(&before, &after),
Damage {
changed: vec![SCOPE_OWNER],
union_frame: Some(coverage),
}
);
assert!(diff_frame(&before, &before).is_empty());
}

/// The contract's union/intersection normal form lowers to one private
/// clip item and clips the enclosed paint without inventing a resource or
/// an isolated layer.
Expand Down
Loading
Loading