Never crash: the engine is panic-free (Rule 9) (#54)
Every unwrap/expect/unreachable in non-test engine code now returns an
EngineError (or a predicate's Err string) or uses a real fallback:
- enabled predicates read the active layer through active_layer_of()
instead of unwrap() after has_layer();
- new raster layers get their pixels through pixels_mut()? instead of
surface_mut().expect("raster");
- layer paths use split_last().ok_or(..)? (Merge Down, Arrange, Ungroup,
Release Clipping), Auto-Align/Auto-Blend/Photomerge carry placements
instead of re-unwrapping them, and "next free name" searches are bounded;
- web export builds its options map directly, quantize_rows has a wasm
tail, parse_script rejects non-array/object JSON, the shape-layer edit
returns an error instead of unreachable!, and the gray composite
profile falls back to sRGB.
The engine denies unwrap/expect/panic/unimplemented/todo/unreachable.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
91ef877f2f
commit
4218d07f40
|
|
@ -307,7 +307,7 @@ fn hdr_toning(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let data: Vec<f32> = px.iter().flat_map(|q| photocraft_raster::from_rgba(&fmt, *q)).collect();
|
let data: Vec<f32> = px.iter().flat_map(|q| photocraft_raster::from_rgba(&fmt, *q)).collect();
|
||||||
let mut bg = Layer::raster("Background", fmt);
|
let mut bg = Layer::raster("Background", fmt);
|
||||||
bg.locks.transparency = true;
|
bg.locks.transparency = true;
|
||||||
bg.surface_mut().expect("raster").write_region(doc.bounds(), &data);
|
crate::pixels_mut(&mut bg)?.write_region(doc.bounds(), &data);
|
||||||
*active = Some(bg.id);
|
*active = Some(bg.id);
|
||||||
doc.layers = vec![bg];
|
doc.layers = vec![bg];
|
||||||
doc.selection = None;
|
doc.selection = None;
|
||||||
|
|
|
||||||
|
|
@ -80,18 +80,18 @@ fn auto_align(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let warped: Vec<(LayerId, Surface, Option<Surface>)> = moves
|
let warped: Vec<(LayerId, Surface, Option<Surface>)> = moves
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&i| {
|
.map(|&i| {
|
||||||
let pl = al.placements[i].as_ref().expect("placed");
|
let pl = al.placements[i].as_ref().ok_or_else(|| EngineError::Other("Auto-Align lost a layer's placement".into()))?;
|
||||||
let l = doc.layer(ids[i]).expect("exists");
|
let l = doc.layer(ids[i]).ok_or(EngineError::NoLayer(ids[i]))?;
|
||||||
let surf = l.surface().expect("raster");
|
let surf = l.surface().ok_or(EngineError::NoLayer(ids[i]))?;
|
||||||
let b = surf.content_bounds();
|
let b = surf.content_bounds();
|
||||||
let px = if b.is_empty() { surf.clone() } else { warp_placed(surf, b, (0.0, 0.0), pl, (0.0, 0.0), interp) };
|
let px = if b.is_empty() { surf.clone() } else { warp_placed(surf, b, (0.0, 0.0), pl, (0.0, 0.0), interp) };
|
||||||
let mask = l.mask.as_ref().map(|m| {
|
let mask = l.mask.as_ref().map(|m| {
|
||||||
let mb = m.surface.content_bounds();
|
let mb = m.surface.content_bounds();
|
||||||
if mb.is_empty() { m.surface.clone() } else { warp_placed(&m.surface, mb, (0.0, 0.0), pl, (0.0, 0.0), Interp::Bilinear) }
|
if mb.is_empty() { m.surface.clone() } else { warp_placed(&m.surface, mb, (0.0, 0.0), pl, (0.0, 0.0), Interp::Bilinear) }
|
||||||
});
|
});
|
||||||
(ids[i], px, mask)
|
Ok((ids[i], px, mask))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect::<Result<_>>()?;
|
||||||
s.edit("Auto-Align Layers", |doc, _| {
|
s.edit("Auto-Align Layers", |doc, _| {
|
||||||
for (id, px, mask) in &warped {
|
for (id, px, mask) in &warped {
|
||||||
let l = doc.layer_mut(*id).ok_or(EngineError::NoLayer(*id))?;
|
let l = doc.layer_mut(*id).ok_or(EngineError::NoLayer(*id))?;
|
||||||
|
|
@ -113,9 +113,9 @@ fn auto_align(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
};
|
};
|
||||||
let aligned: Vec<Value> = moves
|
let aligned: Vec<Value> = moves
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&i| {
|
.filter_map(|&i| {
|
||||||
let pl = al.placements[i].as_ref().expect("placed");
|
let pl = al.placements[i].as_ref()?;
|
||||||
json!({"layer": ids[i].0, "model": model, "matches": inliers[i], "matrix": plane_matrix(pl, (0.0, 0.0)).map(|h| h.0.to_vec())})
|
Some(json!({"layer": ids[i].0, "model": model, "matches": inliers[i], "matrix": plane_matrix(pl, (0.0, 0.0)).map(|h| h.0.to_vec())}))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Ok(json!({"reference": reference.0, "layout": al.layout.name(), "focal": al.focal, "rms": al.rms, "aligned": aligned, "failed": failed}))
|
Ok(json!({"reference": reference.0, "layout": al.layout.name(), "focal": al.focal, "rms": al.rms, "aligned": aligned, "failed": failed}))
|
||||||
|
|
@ -191,7 +191,7 @@ fn auto_blend(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let blended = seamless.then(|| b.composite.read_region(area));
|
let blended = seamless.then(|| b.composite.read_region(area));
|
||||||
(weights, blended)
|
(weights, blended)
|
||||||
};
|
};
|
||||||
let top = *ids.last().expect("two or more");
|
let top = *ids.last().ok_or_else(|| EngineError::Other("Auto-Blend needs two or more layers".into()))?;
|
||||||
let label = "Auto-Blend Layers";
|
let label = "Auto-Blend Layers";
|
||||||
let new_layer = s.edit(label, |doc, active| {
|
let new_layer = s.edit(label, |doc, active| {
|
||||||
for (id, wt) in ids.iter().zip(&weights) {
|
for (id, wt) in ids.iter().zip(&weights) {
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,8 @@ fn fit_canvas(doc: &mut Document) {
|
||||||
|
|
||||||
fn next_artboard_name(doc: &Document) -> String {
|
fn next_artboard_name(doc: &Document) -> String {
|
||||||
let names: Vec<&str> = doc.artboards().into_iter().map(|b| b.1).collect();
|
let names: Vec<&str> = doc.artboards().into_iter().map(|b| b.1).collect();
|
||||||
(1..).map(|n| format!("Artboard {n}")).find(|n| !names.contains(&n.as_str())).expect("infinite")
|
// `names.len() + 1` candidates always include a free one.
|
||||||
|
(1..=names.len() + 1).map(|n| format!("Artboard {n}")).find(|n| !names.contains(&n.as_str())).unwrap_or_else(|| "Artboard".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_artboard(l: &mut Layer, a: Artboard) {
|
fn set_artboard(l: &mut Layer, a: Artboard) {
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ pub fn parse_script(text: &str) -> Result<Vec<(String, Value)>> {
|
||||||
Value::Object(o) => {
|
Value::Object(o) => {
|
||||||
o.get("steps").or_else(|| o.get("action").and_then(|a| a.get("steps").or(Some(a)))).ok_or_else(|| bad(cmd, "script object needs \"steps\""))?
|
o.get("steps").or_else(|| o.get("action").and_then(|a| a.get("steps").or(Some(a)))).ok_or_else(|| bad(cmd, "script object needs \"steps\""))?
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => return Err(bad(cmd, "script JSON must be an array of steps or an object")),
|
||||||
};
|
};
|
||||||
return parse_steps(steps, cmd);
|
return parse_steps(steps, cmd);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,8 @@ pub fn channel_format(doc: &Document) -> PixelFormat {
|
||||||
|
|
||||||
/// Next free "Alpha N" name.
|
/// Next free "Alpha N" name.
|
||||||
fn next_alpha_name(doc: &Document, base: &str) -> String {
|
fn next_alpha_name(doc: &Document, base: &str) -> String {
|
||||||
(1..).map(|n| format!("{base} {n}")).find(|n| !doc.channels.iter().any(|c| &c.name == n)).expect("infinite")
|
// `channels.len() + 1` candidates always include a free one.
|
||||||
|
(1..=doc.channels.len() + 1).map(|n| format!("{base} {n}")).find(|n| !doc.channels.iter().any(|c| &c.name == n)).unwrap_or_else(|| base.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
|
@ -339,12 +340,14 @@ fn ref_planes(doc: &Document, layer: Option<LayerId>, active: Option<LayerId>, r
|
||||||
|
|
||||||
/// One grayscale plane (composites reduce to luminosity, as in Calculations).
|
/// One grayscale plane (composites reduce to luminosity, as in Calculations).
|
||||||
fn gray_plane(doc: &Document, layer: Option<LayerId>, active: Option<LayerId>, r: ChanRef) -> Result<Vec<f32>> {
|
fn gray_plane(doc: &Document, layer: Option<LayerId>, active: Option<LayerId>, r: ChanRef) -> Result<Vec<f32>> {
|
||||||
let planes = ref_planes(doc, layer, active, r)?;
|
let mut planes = ref_planes(doc, layer, active, r)?;
|
||||||
if planes.len() == 1 {
|
if planes.len() == 1
|
||||||
return Ok(planes.into_iter().next().expect("one plane"));
|
&& let Some(only) = planes.pop()
|
||||||
|
{
|
||||||
|
return Ok(only);
|
||||||
}
|
}
|
||||||
let fmt = doc.pixel_format();
|
let fmt = doc.pixel_format();
|
||||||
let n = planes[0].len();
|
let n = planes.first().map_or(0, Vec::len);
|
||||||
let mut px = vec![0.0f32; fmt.channels()];
|
let mut px = vec![0.0f32; fmt.channels()];
|
||||||
Ok((0..n)
|
Ok((0..n)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
|
|
@ -596,17 +599,12 @@ fn has_selection(s: &Session) -> std::result::Result<(), String> {
|
||||||
has_doc(s)?;
|
has_doc(s)?;
|
||||||
if s.active().is_some_and(|d| d.doc.selection.is_some()) { Ok(()) } else { Err("no selection".into()) }
|
if s.active().is_some_and(|d| d.doc.selection.is_some()) { Ok(()) } else { Err("no selection".into()) }
|
||||||
}
|
}
|
||||||
fn has_layer(s: &Session) -> std::result::Result<(), String> {
|
|
||||||
let d = s.active().ok_or("no document open")?;
|
|
||||||
d.active_layer.filter(|id| d.doc.layer(*id).is_some()).map(|_| ()).ok_or_else(|| "no active layer".into())
|
|
||||||
}
|
|
||||||
fn has_spot(s: &Session) -> std::result::Result<(), String> {
|
fn has_spot(s: &Session) -> std::result::Result<(), String> {
|
||||||
has_doc(s)?;
|
has_doc(s)?;
|
||||||
if s.active().is_some_and(|d| d.doc.channels.iter().any(|c| c.spot.is_some())) { Ok(()) } else { Err("the document has no spot channels".into()) }
|
if s.active().is_some_and(|d| d.doc.channels.iter().any(|c| c.spot.is_some())) { Ok(()) } else { Err("the document has no spot channels".into()) }
|
||||||
}
|
}
|
||||||
fn can_split(s: &Session) -> std::result::Result<(), String> {
|
fn can_split(s: &Session) -> std::result::Result<(), String> {
|
||||||
has_doc(s)?;
|
let d = s.active().ok_or("no document open")?;
|
||||||
let d = s.active().expect("checked");
|
|
||||||
if color_count(&d.doc) + d.doc.channels.len() < 2 { Err("only one channel to split".into()) } else { Ok(()) }
|
if color_count(&d.doc) + d.doc.channels.len() < 2 { Err("only one channel to split".into()) } else { Ok(()) }
|
||||||
}
|
}
|
||||||
fn can_merge(s: &Session) -> std::result::Result<(), String> {
|
fn can_merge(s: &Session) -> std::result::Result<(), String> {
|
||||||
|
|
@ -691,8 +689,9 @@ fn load_selection(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
return Err(bad(cmd, "missing `channel`"));
|
return Err(bad(cmd, "missing `channel`"));
|
||||||
}
|
}
|
||||||
// Composite and colour channels load their luminosity / values, as ⌘-clicking them does.
|
// Composite and colour channels load their luminosity / values, as ⌘-clicking them does.
|
||||||
let plane = source(s, p, cmd, true)?.remove(0);
|
let plane = source(s, p, cmd, true)?.into_iter().next().ok_or_else(|| bad(cmd, "the channel has no pixels"))?;
|
||||||
let label = match p.get("channel").and_then(|v| parse_ref(v, &s.active().expect("source checked").doc)) {
|
let doc = &s.active().ok_or(EngineError::NoDocument)?.doc;
|
||||||
|
let label = match p.get("channel").and_then(|v| parse_ref(v, doc)) {
|
||||||
Some(ChanRef::Transparency) => "Load Transparency",
|
Some(ChanRef::Transparency) => "Load Transparency",
|
||||||
Some(ChanRef::LayerMask) => "Load Layer Mask",
|
Some(ChanRef::LayerMask) => "Load Layer Mask",
|
||||||
_ => "Load Selection",
|
_ => "Load Selection",
|
||||||
|
|
@ -910,7 +909,7 @@ fn delete_channel(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let doc = &s.active().ok_or(EngineError::NoDocument)?.doc;
|
let doc = &s.active().ok_or(EngineError::NoDocument)?.doc;
|
||||||
let i = match p.get("channel") {
|
let i = match p.get("channel") {
|
||||||
Some(_) => alpha_index(doc, p, "channel", cmd)?,
|
Some(_) => alpha_index(doc, p, "channel", cmd)?,
|
||||||
None => match s.active().expect("checked").channel_view.target {
|
None => match s.active().ok_or(EngineError::NoDocument)?.channel_view.target {
|
||||||
ChannelTarget::Alpha(i) => i,
|
ChannelTarget::Alpha(i) => i,
|
||||||
_ => return Err(bad(cmd, "missing `channel`")),
|
_ => return Err(bad(cmd, "missing `channel`")),
|
||||||
},
|
},
|
||||||
|
|
@ -1050,7 +1049,7 @@ fn set_visible(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let r = p.get("channel").and_then(|v| parse_ref(v, doc)).ok_or_else(|| bad(cmd, "missing or unknown `channel`"))?;
|
let r = p.get("channel").and_then(|v| parse_ref(v, doc)).ok_or_else(|| bad(cmd, "missing or unknown `channel`"))?;
|
||||||
let colors = color_count(doc);
|
let colors = color_count(doc);
|
||||||
let n = doc.channels.len();
|
let n = doc.channels.len();
|
||||||
let st = s.active_mut().expect("checked");
|
let st = s.active_mut().ok_or(EngineError::NoDocument)?;
|
||||||
let v = &mut st.channel_view;
|
let v = &mut st.channel_view;
|
||||||
let visible = |cur: bool| p.get("visible").and_then(Value::as_bool).unwrap_or(!cur);
|
let visible = |cur: bool| p.get("visible").and_then(Value::as_bool).unwrap_or(!cur);
|
||||||
match r {
|
match r {
|
||||||
|
|
@ -1151,7 +1150,7 @@ fn merge_spot(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let doc = &s.active().ok_or(EngineError::NoDocument)?.doc;
|
let doc = &s.active().ok_or(EngineError::NoDocument)?.doc;
|
||||||
let i = match p.get("channel") {
|
let i = match p.get("channel") {
|
||||||
Some(_) => alpha_index(doc, p, "channel", cmd)?,
|
Some(_) => alpha_index(doc, p, "channel", cmd)?,
|
||||||
None => match s.active().expect("checked").channel_view.target {
|
None => match s.active().ok_or(EngineError::NoDocument)?.channel_view.target {
|
||||||
ChannelTarget::Alpha(i) => i,
|
ChannelTarget::Alpha(i) => i,
|
||||||
_ => doc.channels.iter().position(|c| c.spot.is_some()).ok_or_else(|| bad(cmd, "no spot channel"))?,
|
_ => doc.channels.iter().position(|c| c.spot.is_some()).ok_or_else(|| bad(cmd, "no spot channel"))?,
|
||||||
},
|
},
|
||||||
|
|
@ -1563,9 +1562,8 @@ fn has_apply_target(s: &Session) -> std::result::Result<(), String> {
|
||||||
if edits_channel(s) {
|
if edits_channel(s) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
has_layer(s)?;
|
let d = s.active().ok_or("no document open")?;
|
||||||
let d = s.active().expect("checked");
|
let l = d.active_layer.and_then(|id| d.doc.layer(id)).ok_or("no active layer")?;
|
||||||
let l = d.doc.layer(d.active_layer.expect("checked")).expect("checked");
|
|
||||||
if matches!(l.content, LayerContent::Raster(_)) {
|
if matches!(l.content, LayerContent::Raster(_)) {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -408,7 +408,8 @@ pub fn composite_profile(doc: &Document) -> Arc<Profile> {
|
||||||
ColorSpace::Rgb => document_profile(doc),
|
ColorSpace::Rgb => document_profile(doc),
|
||||||
ColorSpace::Gray => {
|
ColorSpace::Gray => {
|
||||||
let p = document_profile(doc);
|
let p = document_profile(doc);
|
||||||
p.gray_as_rgb().map(Arc::new).unwrap_or_else(|| Arc::new(Builtin::SGray.profile().gray_as_rgb().expect("sGray is a TRC profile")))
|
// sGray is a TRC profile, so its RGB view always exists; sRGB is the last resort.
|
||||||
|
p.gray_as_rgb().or_else(|| Builtin::SGray.profile().gray_as_rgb()).map(Arc::new).unwrap_or_else(|| Arc::new(Builtin::Srgb.profile().clone()))
|
||||||
}
|
}
|
||||||
_ => Arc::new(Builtin::Srgb.profile().clone()),
|
_ => Arc::new(Builtin::Srgb.profile().clone()),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,7 @@ fn has_layer(s: &Session) -> std::result::Result<(), String> {
|
||||||
d.active_layer.filter(|id| d.doc.layer(*id).is_some()).map(|_| ()).ok_or_else(|| "no active layer".into())
|
d.active_layer.filter(|id| d.doc.layer(*id).is_some()).map(|_| ()).ok_or_else(|| "no active layer".into())
|
||||||
}
|
}
|
||||||
fn has_pixel_layer(s: &Session) -> std::result::Result<(), String> {
|
fn has_pixel_layer(s: &Session) -> std::result::Result<(), String> {
|
||||||
has_layer(s)?;
|
let l = crate::active_layer_of(s)?;
|
||||||
let d = s.active().unwrap();
|
|
||||||
let l = d.doc.layer(d.active_layer.unwrap()).unwrap();
|
|
||||||
if matches!(l.content, LayerContent::Raster(_)) { Ok(()) } else { Err(format!("active layer is a {} layer, not a pixel layer", l.content.kind_name())) }
|
if matches!(l.content, LayerContent::Raster(_)) { Ok(()) } else { Err(format!("active layer is a {} layer, not a pixel layer", l.content.kind_name())) }
|
||||||
}
|
}
|
||||||
/// A pixel layer, or a targeted alpha channel / Quick Mask (adjustments and fills apply to it).
|
/// A pixel layer, or a targeted alpha channel / Quick Mask (adjustments and fills apply to it).
|
||||||
|
|
@ -56,9 +54,7 @@ pub(crate) fn has_paintable(s: &Session) -> std::result::Result<(), String> {
|
||||||
if crate::channel_cmds::edits_channel(s) {
|
if crate::channel_cmds::edits_channel(s) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
has_layer(s)?;
|
let l = crate::active_layer_of(s)?;
|
||||||
let d = s.active().unwrap();
|
|
||||||
let l = d.doc.layer(d.active_layer.unwrap()).unwrap();
|
|
||||||
if matches!(l.content, LayerContent::Raster(_)) || l.mask.is_some() {
|
if matches!(l.content, LayerContent::Raster(_)) || l.mask.is_some() {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -623,17 +619,17 @@ fn build() -> Vec<CommandSpec> {
|
||||||
let id = layer_param(s, p)?;
|
let id = layer_param(s, p)?;
|
||||||
s.edit("Merge Down", |doc, active| {
|
s.edit("Merge Down", |doc, active| {
|
||||||
let path = doc.path_of(id).ok_or(EngineError::NoLayer(id))?;
|
let path = doc.path_of(id).ok_or(EngineError::NoLayer(id))?;
|
||||||
let idx = *path.last().unwrap();
|
let Some((&idx, parent)) = path.split_last() else { return Err(EngineError::NoLayer(id)) };
|
||||||
if idx == 0 {
|
if idx == 0 {
|
||||||
return Err(EngineError::Other("no layer below to merge into".into()));
|
return Err(EngineError::Other("no layer below to merge into".into()));
|
||||||
}
|
}
|
||||||
let mut below = path.clone();
|
let mut below = parent.to_vec();
|
||||||
*below.last_mut().unwrap() = idx - 1;
|
below.push(idx - 1);
|
||||||
let lower = doc.layer_at(&below).ok_or(EngineError::NoLayer(id))?.clone();
|
let lower = doc.layer_at(&below).ok_or(EngineError::NoLayer(id))?.clone();
|
||||||
let upper = doc.layer(id).ok_or(EngineError::NoLayer(id))?.clone();
|
let upper = doc.layer(id).ok_or(EngineError::NoLayer(id))?.clone();
|
||||||
let merged = pixels::merge_down(doc.bounds(), &lower, &upper, doc.pixel_format());
|
let merged = pixels::merge_down(doc.bounds(), &lower, &upper, doc.pixel_format());
|
||||||
doc.remove(id);
|
doc.remove(id);
|
||||||
*doc.layer_at_mut(&below).unwrap() = merged;
|
*doc.layer_at_mut(&below).ok_or(EngineError::NoLayer(lower.id))? = merged;
|
||||||
*active = Some(lower.id);
|
*active = Some(lower.id);
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -646,7 +642,7 @@ fn build() -> Vec<CommandSpec> {
|
||||||
let data: Vec<f32> = buf.px.iter().flat_map(|p| photocraft_raster::from_rgba(&fmt, *p)).collect();
|
let data: Vec<f32> = buf.px.iter().flat_map(|p| photocraft_raster::from_rgba(&fmt, *p)).collect();
|
||||||
let mut bg = Layer::raster("Background", fmt);
|
let mut bg = Layer::raster("Background", fmt);
|
||||||
bg.locks.transparency = true;
|
bg.locks.transparency = true;
|
||||||
bg.surface_mut().unwrap().write_region(doc.bounds(), &data);
|
crate::pixels_mut(&mut bg)?.write_region(doc.bounds(), &data);
|
||||||
*active = Some(bg.id);
|
*active = Some(bg.id);
|
||||||
doc.layers = vec![bg];
|
doc.layers = vec![bg];
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -725,11 +721,13 @@ fn build() -> Vec<CommandSpec> {
|
||||||
g.push(layer);
|
g.push(layer);
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
let (&last, parent) = path.split_last().expect("non-empty path");
|
let (&last, parent) = path.split_last().ok_or(EngineError::NoLayer(target))?;
|
||||||
let sib = if parent.is_empty() {
|
let sib = if parent.is_empty() {
|
||||||
&mut doc.layers
|
&mut doc.layers
|
||||||
} else {
|
} else {
|
||||||
doc.layer_at_mut(parent).and_then(|t| t.children_mut()).expect("parent is a group")
|
doc.layer_at_mut(parent)
|
||||||
|
.and_then(|t| t.children_mut())
|
||||||
|
.ok_or_else(|| EngineError::Other("target's parent is not a group".into()))?
|
||||||
};
|
};
|
||||||
let at = if other == "below" { last } else { last + 1 };
|
let at = if other == "below" { last } else { last + 1 };
|
||||||
sib.insert(at.min(sib.len()), layer);
|
sib.insert(at.min(sib.len()), layer);
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@ fn new_comp(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let d = s.active().ok_or(EngineError::NoDocument)?;
|
let d = s.active().ok_or(EngineError::NoDocument)?;
|
||||||
let name = p.get("name").and_then(Value::as_str).map(str::to_string).unwrap_or_else(|| {
|
let name = p.get("name").and_then(Value::as_str).map(str::to_string).unwrap_or_else(|| {
|
||||||
let names: Vec<&str> = d.doc.layer_comps.iter().map(|c| c.name.as_str()).collect();
|
let names: Vec<&str> = d.doc.layer_comps.iter().map(|c| c.name.as_str()).collect();
|
||||||
(1..).map(|n| format!("Layer Comp {n}")).find(|n| !names.contains(&n.as_str())).expect("infinite")
|
// `names.len() + 1` candidates always include a free one.
|
||||||
|
(1..=names.len() + 1).map(|n| format!("Layer Comp {n}")).find(|n| !names.contains(&n.as_str())).unwrap_or_else(|| "Layer Comp".into())
|
||||||
});
|
});
|
||||||
let comment = p.get("comment").and_then(Value::as_str).unwrap_or_default().to_string();
|
let comment = p.get("comment").and_then(Value::as_str).unwrap_or_default().to_string();
|
||||||
let (v, pos, a) = (flag(p, "visibility", true), flag(p, "position", true), flag(p, "appearance", true));
|
let (v, pos, a) = (flag(p, "visibility", true), flag(p, "position", true), flag(p, "appearance", true));
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ fn paste(s: &mut Session, p: &Value, in_place: bool) -> Result<Value> {
|
||||||
let surf = if moved.format() == target { moved } else { moved.convert(target) };
|
let surf = if moved.format() == target { moved } else { moved.convert(target) };
|
||||||
let id = s.edit("Paste", |doc, active| {
|
let id = s.edit("Paste", |doc, active| {
|
||||||
let mut l = Layer::raster(doc.next_layer_name("Layer"), target);
|
let mut l = Layer::raster(doc.next_layer_name("Layer"), target);
|
||||||
*l.surface_mut().expect("raster") = surf;
|
*crate::pixels_mut(&mut l)? = surf;
|
||||||
let id = doc.insert_above(*active, l);
|
let id = doc.insert_above(*active, l);
|
||||||
*active = Some(id);
|
*active = Some(id);
|
||||||
doc.selection = None;
|
doc.selection = None;
|
||||||
|
|
@ -161,7 +161,7 @@ fn layer_via(s: &mut Session, cut: bool) -> Result<Value> {
|
||||||
clear_selected(doc, id)?;
|
clear_selected(doc, id)?;
|
||||||
}
|
}
|
||||||
let mut l = Layer::raster(doc.next_layer_name("Layer"), clip.surface.format());
|
let mut l = Layer::raster(doc.next_layer_name("Layer"), clip.surface.format());
|
||||||
*l.surface_mut().expect("raster") = clip.surface;
|
*crate::pixels_mut(&mut l)? = clip.surface;
|
||||||
let nid = doc.insert_above(Some(id), l);
|
let nid = doc.insert_above(Some(id), l);
|
||||||
*active = Some(nid);
|
*active = Some(nid);
|
||||||
Ok(nid)
|
Ok(nid)
|
||||||
|
|
@ -188,7 +188,7 @@ fn merge_visible(s: &mut Session) -> Result<Value> {
|
||||||
let data: Vec<f32> = buf.px.iter().flat_map(|p| photocraft_raster::from_rgba(&fmt, *p)).collect();
|
let data: Vec<f32> = buf.px.iter().flat_map(|p| photocraft_raster::from_rgba(&fmt, *p)).collect();
|
||||||
let mut merged = Layer::raster(base.name.clone(), fmt);
|
let mut merged = Layer::raster(base.name.clone(), fmt);
|
||||||
merged.locks = base.locks;
|
merged.locks = base.locks;
|
||||||
let surf = merged.surface_mut().expect("raster");
|
let surf = crate::pixels_mut(&mut merged)?;
|
||||||
surf.write_region(doc.bounds(), &data);
|
surf.write_region(doc.bounds(), &data);
|
||||||
surf.prune();
|
surf.prune();
|
||||||
let mid = merged.id;
|
let mid = merged.id;
|
||||||
|
|
|
||||||
|
|
@ -417,7 +417,7 @@ fn ungroup(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
return Err(EngineError::Other("the active layer is not a group".into()));
|
return Err(EngineError::Other("the active layer is not a group".into()));
|
||||||
};
|
};
|
||||||
let children = group.children.clone();
|
let children = group.children.clone();
|
||||||
let (&idx, parent) = path.split_last().unwrap();
|
let (&idx, parent) = path.split_last().ok_or(EngineError::NoLayer(id))?;
|
||||||
let sib = if parent.is_empty() { &mut doc.layers } else { doc.layer_at_mut(parent).and_then(Layer::children_mut).ok_or(EngineError::NoLayer(id))? };
|
let sib = if parent.is_empty() { &mut doc.layers } else { doc.layer_at_mut(parent).and_then(Layer::children_mut).ok_or(EngineError::NoLayer(id))? };
|
||||||
sib.remove(idx);
|
sib.remove(idx);
|
||||||
let top = children.last().map(|l| l.id);
|
let top = children.last().map(|l| l.id);
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,9 @@ fn frame_from_layers(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
}
|
}
|
||||||
let grouped = s.execute("layer.new.groupFromLayers", Value::Object(gp))?;
|
let grouped = s.execute("layer.new.groupFromLayers", Value::Object(gp))?;
|
||||||
let gid = LayerId(grouped.get("layer").and_then(Value::as_u64).ok_or_else(|| EngineError::Other("grouping did not return a layer".into()))?);
|
let gid = LayerId(grouped.get("layer").and_then(Value::as_u64).ok_or_else(|| EngineError::Other("grouping did not return a layer".into()))?);
|
||||||
let rect = crate::layer_multi_cmds::layer_bounds(s.active().ok_or(EngineError::NoDocument)?.doc.layer(gid).ok_or(EngineError::NoLayer(gid))?)
|
let d = s.active().ok_or(EngineError::NoDocument)?;
|
||||||
.filter(|r| !r.is_empty())
|
let rect =
|
||||||
.unwrap_or_else(|| s.active().unwrap().doc.bounds());
|
crate::layer_multi_cmds::layer_bounds(d.doc.layer(gid).ok_or(EngineError::NoLayer(gid))?).filter(|r| !r.is_empty()).unwrap_or_else(|| d.doc.bounds());
|
||||||
|
|
||||||
s.edit("Frame from Layers", |doc, _| {
|
s.edit("Frame from Layers", |doc, _| {
|
||||||
let g = doc.layer_mut(gid).ok_or(EngineError::NoLayer(gid))?;
|
let g = doc.layer_mut(gid).ok_or(EngineError::NoLayer(gid))?;
|
||||||
|
|
|
||||||
|
|
@ -606,7 +606,7 @@ fn create_layer(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let n = below.len() + above.len();
|
let n = below.len() + above.len();
|
||||||
// Insert in order: below layers under the source, clipped layers above it.
|
// Insert in order: below layers under the source, clipped layers above it.
|
||||||
let path = doc.path_of(id).ok_or(EngineError::NoLayer(id))?;
|
let path = doc.path_of(id).ok_or(EngineError::NoLayer(id))?;
|
||||||
let (&at, parent) = path.split_last().expect("non-empty path");
|
let (&at, parent) = path.split_last().ok_or_else(|| other("bad layer path"))?;
|
||||||
let sib =
|
let sib =
|
||||||
if parent.is_empty() { &mut doc.layers } else { doc.layer_at_mut(parent).and_then(|t| t.children_mut()).ok_or_else(|| other("bad layer path"))? };
|
if parent.is_empty() { &mut doc.layers } else { doc.layer_at_mut(parent).and_then(|t| t.children_mut()).ok_or_else(|| other("bad layer path"))? };
|
||||||
let src = &mut sib[at];
|
let src = &mut sib[at];
|
||||||
|
|
|
||||||
|
|
@ -521,7 +521,7 @@ pub fn group_layers(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
for id in &ids {
|
for id in &ids {
|
||||||
children.push(doc.remove(*id).ok_or(EngineError::NoLayer(*id))?);
|
children.push(doc.remove(*id).ok_or(EngineError::NoLayer(*id))?);
|
||||||
}
|
}
|
||||||
*doc.layer_mut(gid).and_then(Layer::children_mut).expect("new group") = children;
|
*doc.layer_mut(gid).and_then(Layer::children_mut).ok_or(EngineError::NoLayer(gid))? = children;
|
||||||
*active = Some(gid);
|
*active = Some(gid);
|
||||||
Ok(gid)
|
Ok(gid)
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -536,7 +536,7 @@ fn merge_layers(s: &mut Session) -> Result<Value> {
|
||||||
}
|
}
|
||||||
let mid = s.edit("Merge Layers", |doc, active| {
|
let mid = s.edit("Merge Layers", |doc, active| {
|
||||||
let ids = top_level(doc, &sel);
|
let ids = top_level(doc, &sel);
|
||||||
let top_id = *ids.last().expect("two or more");
|
let top_id = *ids.last().ok_or_else(|| EngineError::Other("Merge Layers needs two or more layers".into()))?;
|
||||||
let top = doc.layer(top_id).ok_or(EngineError::NoLayer(top_id))?.clone();
|
let top = doc.layer(top_id).ok_or(EngineError::NoLayer(top_id))?.clone();
|
||||||
let mut solo = doc.clone();
|
let mut solo = doc.clone();
|
||||||
solo.layers = ids.iter().filter_map(|id| doc.layer(*id)).filter(|l| l.visible).cloned().collect();
|
solo.layers = ids.iter().filter_map(|id| doc.layer(*id)).filter(|l| l.visible).cloned().collect();
|
||||||
|
|
@ -549,7 +549,7 @@ fn merge_layers(s: &mut Session) -> Result<Value> {
|
||||||
let data: Vec<f32> = buf.px.iter().flat_map(|p| photocraft_raster::from_rgba(&fmt, *p)).collect();
|
let data: Vec<f32> = buf.px.iter().flat_map(|p| photocraft_raster::from_rgba(&fmt, *p)).collect();
|
||||||
let mut merged = Layer::raster(top.name.clone(), fmt);
|
let mut merged = Layer::raster(top.name.clone(), fmt);
|
||||||
merged.locks = top.locks;
|
merged.locks = top.locks;
|
||||||
let surf = merged.surface_mut().expect("raster");
|
let surf = crate::pixels_mut(&mut merged)?;
|
||||||
surf.write_region(doc.bounds(), &data);
|
surf.write_region(doc.bounds(), &data);
|
||||||
surf.prune();
|
surf.prune();
|
||||||
let mid = merged.id;
|
let mid = merged.id;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
//! [`Session::execute`] entry point: the egui UI, the CLI, the remote-control channel and the MCP
|
//! [`Session::execute`] entry point: the egui UI, the CLI, the remote-control channel and the MCP
|
||||||
//! server. This is what makes the UI swappable and the app fully scriptable.
|
//! server. This is what makes the UI swappable and the app fully scriptable.
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||||
|
|
||||||
pub mod adjust_cmds;
|
pub mod adjust_cmds;
|
||||||
pub mod align_cmds;
|
pub mod align_cmds;
|
||||||
|
|
@ -97,6 +98,19 @@ pub enum EngineError {
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, EngineError>;
|
pub type Result<T> = std::result::Result<T, EngineError>;
|
||||||
|
|
||||||
|
/// The pixels of a raster layer (typically one a command just created), as an error instead
|
||||||
|
/// of a panic if the layer has none.
|
||||||
|
pub(crate) fn pixels_mut(l: &mut photocraft_doc::Layer) -> Result<&mut photocraft_raster::Surface> {
|
||||||
|
let id = l.id;
|
||||||
|
l.surface_mut().ok_or_else(|| EngineError::Other(format!("layer {id:?} has no pixels")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The active document's active layer, for `enabled` predicates.
|
||||||
|
pub(crate) fn active_layer_of(s: &Session) -> std::result::Result<&photocraft_doc::Layer, String> {
|
||||||
|
let d = s.active().ok_or("no document open")?;
|
||||||
|
d.active_layer.and_then(|id| d.doc.layer(id)).ok_or_else(|| "no active layer".into())
|
||||||
|
}
|
||||||
|
|
||||||
/// Per-document editing state.
|
/// Per-document editing state.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct DocState {
|
pub struct DocState {
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,8 @@ fn rotate_arbitrary(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let fmt = doc.pixel_format();
|
let fmt = doc.pixel_format();
|
||||||
for l in doc.layers.iter_mut() {
|
for l in doc.layers.iter_mut() {
|
||||||
let background = l.name == "Background" && l.locks.position && matches!(l.content, LayerContent::Raster(_));
|
let background = l.name == "Background" && l.locks.position && matches!(l.content, LayerContent::Raster(_));
|
||||||
if background {
|
if background && let Some(surf) = l.surface_mut() {
|
||||||
// The Background stays a Background: rotated pixels over the background colour.
|
// The Background stays a Background: rotated pixels over the background colour.
|
||||||
let surf = l.surface_mut().expect("raster");
|
|
||||||
let src = surf.content_bounds();
|
let src = surf.content_bounds();
|
||||||
let rotated = photocraft_algo::transform::warp_surface(surf, src, &h, interp);
|
let rotated = photocraft_algo::transform::warp_surface(surf, src, &h, interp);
|
||||||
let mut base = Surface::new(fmt);
|
let mut base = Surface::new(fmt);
|
||||||
|
|
@ -146,7 +145,10 @@ fn single_layer(doc: &mut Document, active: &mut Option<LayerId>, mode: ColorMod
|
||||||
l.locks.transparency = true;
|
l.locks.transparency = true;
|
||||||
l.locks.position = true;
|
l.locks.position = true;
|
||||||
}
|
}
|
||||||
l.surface_mut().expect("raster").write_region(doc.bounds(), &data);
|
// A new raster layer always has pixels.
|
||||||
|
if let Some(surf) = l.surface_mut() {
|
||||||
|
surf.write_region(doc.bounds(), &data);
|
||||||
|
}
|
||||||
*active = Some(l.id);
|
*active = Some(l.id);
|
||||||
doc.layers = vec![l];
|
doc.layers = vec![l];
|
||||||
for ch in doc.channels.iter_mut() {
|
for ch in doc.channels.iter_mut() {
|
||||||
|
|
|
||||||
|
|
@ -108,13 +108,12 @@ fn inks_of(doc: &Document) -> Vec<(String, Color, Vec<f32>)> {
|
||||||
.map(|((n, c), p)| (n.to_string(), c, p))
|
.map(|((n, c), p)| (n.to_string(), c, p))
|
||||||
.collect(),
|
.collect(),
|
||||||
ColorMode::Lab => planes.iter().enumerate().map(|(k, p)| (format!("Alpha {}", k + 1), Color::BLACK, inv(p))).collect(),
|
ColorMode::Lab => planes.iter().enumerate().map(|(k, p)| (format!("Alpha {}", k + 1), Color::BLACK, inv(p))).collect(),
|
||||||
ColorMode::Duotone if doc.duotone.as_ref().is_some_and(|d| !d.inks.is_empty()) => {
|
ColorMode::Duotone if doc.duotone.as_ref().is_some_and(|d| !d.inks.is_empty()) => doc
|
||||||
let d = doc.duotone.as_ref().expect("checked");
|
.duotone
|
||||||
d.inks
|
.iter()
|
||||||
.iter()
|
.flat_map(|d| d.inks.iter())
|
||||||
.map(|ink| (ink.name.clone(), Color::rgb(ink.color[0], ink.color[1], ink.color[2]), planes[0].iter().map(|g| ink.density(1.0 - g)).collect()))
|
.map(|ink| (ink.name.clone(), Color::rgb(ink.color[0], ink.color[1], ink.color[2]), planes[0].iter().map(|g| ink.density(1.0 - g)).collect()))
|
||||||
.collect()
|
.collect(),
|
||||||
}
|
|
||||||
_ => vec![("Black".to_string(), Color::BLACK, inv(&planes[0]))],
|
_ => vec![("Black".to_string(), Color::BLACK, inv(&planes[0]))],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -227,8 +227,7 @@ pub(crate) fn seam_blend(warped: &[Surface], canvas: Rect, order: &[usize], seam
|
||||||
}
|
}
|
||||||
for y in 0..roi.h {
|
for y in 0..roi.h {
|
||||||
for x in 0..roi.w {
|
for x in 0..roi.w {
|
||||||
let r2 = if vig {
|
let r2 = if vig && let Some((pl, sizes)) = geo {
|
||||||
let (pl, sizes) = geo.expect("vignette needs geometry");
|
|
||||||
pl[i].inverse((roi.x0 + x as i32) as f64 + 0.5, (roi.y0 + y as i32) as f64 + 0.5).map_or(0.0, |(sx, sy)| {
|
pl[i].inverse((roi.x0 + x as i32) as f64 + 0.5, (roi.y0 + y as i32) as f64 + 0.5).map_or(0.0, |(sx, sy)| {
|
||||||
let (w, h) = (sizes[i].0 as f64, sizes[i].1 as f64);
|
let (w, h) = (sizes[i].0 as f64, sizes[i].1 as f64);
|
||||||
let (dx, dy) = (sx - w / 2.0, sy - h / 2.0);
|
let (dx, dy) = (sx - w / 2.0, sy - h / 2.0);
|
||||||
|
|
@ -410,11 +409,13 @@ fn photomerge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
.ok_or_else(|| EngineError::Other("Photomerge couldn't find enough matching detail between the images".into()))?;
|
.ok_or_else(|| EngineError::Other("Photomerge couldn't find enough matching detail between the images".into()))?;
|
||||||
let t_reg = t0.ms() / 1000.0;
|
let t_reg = t0.ms() / 1000.0;
|
||||||
let placed: Vec<usize> = (0..srcs.len()).filter(|&i| al.placements[i].is_some()).collect();
|
let placed: Vec<usize> = (0..srcs.len()).filter(|&i| al.placements[i].is_some()).collect();
|
||||||
|
// Parallel to `placed`.
|
||||||
|
let placements: Vec<&Placement> = placed.iter().filter_map(|&i| al.placements[i].as_ref()).collect();
|
||||||
let failed: Vec<String> = (0..srcs.len()).filter(|&i| al.placements[i].is_none()).map(|i| srcs[i].name.clone()).collect();
|
let failed: Vec<String> = (0..srcs.len()).filter(|&i| al.placements[i].is_none()).map(|i| srcs[i].name.clone()).collect();
|
||||||
// Canvas: union of the placed images.
|
// Canvas: union of the placed images.
|
||||||
let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
|
let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
|
||||||
for &i in &placed {
|
for (&i, pl) in placed.iter().zip(&placements) {
|
||||||
let b = al.placements[i].as_ref().expect("placed").bounds(srcs[i].w as f64, srcs[i].h as f64);
|
let b = pl.bounds(srcs[i].w as f64, srcs[i].h as f64);
|
||||||
x0 = x0.min(b[0]);
|
x0 = x0.min(b[0]);
|
||||||
y0 = y0.min(b[1]);
|
y0 = y0.min(b[1]);
|
||||||
x1 = x1.max(b[2]);
|
x1 = x1.max(b[2]);
|
||||||
|
|
@ -427,18 +428,10 @@ fn photomerge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let canvas = Rect::new(0, 0, cw as i32, ch as i32);
|
let canvas = Rect::new(0, 0, cw as i32, ch as i32);
|
||||||
let offset = (-x0.floor(), -y0.floor());
|
let offset = (-x0.floor(), -y0.floor());
|
||||||
let warped: Vec<Surface> = {
|
let warped: Vec<Surface> = {
|
||||||
let jobs: Vec<usize> = placed.clone();
|
placed
|
||||||
jobs.iter()
|
.iter()
|
||||||
.map(|&i| {
|
.zip(&placements)
|
||||||
warp_placed(
|
.map(|(&i, pl)| warp_placed(&srcs[i].surf, Rect::new(0, 0, srcs[i].w as i32, srcs[i].h as i32), (0.0, 0.0), pl, offset, Interp::Bicubic))
|
||||||
&srcs[i].surf,
|
|
||||||
Rect::new(0, 0, srcs[i].w as i32, srcs[i].h as i32),
|
|
||||||
(0.0, 0.0),
|
|
||||||
al.placements[i].as_ref().expect("placed"),
|
|
||||||
offset,
|
|
||||||
Interp::Bicubic,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
let t_warp = t0.ms() / 1000.0;
|
let t_warp = t0.ms() / 1000.0;
|
||||||
|
|
@ -447,7 +440,7 @@ fn photomerge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let mut info = json!({});
|
let mut info = json!({});
|
||||||
let ref_pos = placed.iter().position(|&i| i == al.reference).unwrap_or(0);
|
let ref_pos = placed.iter().position(|&i| i == al.reference).unwrap_or(0);
|
||||||
if blend {
|
if blend {
|
||||||
let pls: Vec<Placement> = placed.iter().map(|&i| al.placements[i].clone().expect("placed")).collect();
|
let pls: Vec<Placement> = placements.iter().map(|p| (*p).clone()).collect();
|
||||||
let sizes: Vec<(usize, usize)> = placed.iter().map(|&i| (srcs[i].w, srcs[i].h)).collect();
|
let sizes: Vec<(usize, usize)> = placed.iter().map(|&i| (srcs[i].w, srcs[i].h)).collect();
|
||||||
let order = seam_order(&warped, ref_pos);
|
let order = seam_order(&warped, ref_pos);
|
||||||
let geo = vignette.then_some((pls.as_slice(), sizes.as_slice(), offset));
|
let geo = vignette.then_some((pls.as_slice(), sizes.as_slice(), offset));
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,10 @@ pub fn parse(src: &str) -> std::result::Result<Path, String> {
|
||||||
s.closed = closed;
|
s.closed = closed;
|
||||||
if closed && s.knots.len() > 1 {
|
if closed && s.knots.len() > 1 {
|
||||||
let (f, l) = (s.knots[0].anchor, s.knots[s.knots.len() - 1].anchor);
|
let (f, l) = (s.knots[0].anchor, s.knots[s.knots.len() - 1].anchor);
|
||||||
if (f.x - l.x).abs() < 1e-9 && (f.y - l.y).abs() < 1e-9 {
|
if (f.x - l.x).abs() < 1e-9
|
||||||
let last = s.knots.pop().expect("len > 1");
|
&& (f.y - l.y).abs() < 1e-9
|
||||||
|
&& let Some(last) = s.knots.pop()
|
||||||
|
{
|
||||||
s.knots[0].in_ctrl = last.in_ctrl;
|
s.knots[0].in_ctrl = last.in_ctrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +101,7 @@ pub fn parse(src: &str) -> std::result::Result<Path, String> {
|
||||||
"C" | "Q" => {
|
"C" | "Q" => {
|
||||||
let s = cur.as_mut().ok_or("curve before M")?;
|
let s = cur.as_mut().ok_or("curve before M")?;
|
||||||
while is_num(i) {
|
while is_num(i) {
|
||||||
let last = s.knots.last().expect("M pushed a knot").anchor;
|
let last = s.knots.last().ok_or("curve before M")?.anchor;
|
||||||
let (c1, c2, p) = if t == "C" {
|
let (c1, c2, p) = if t == "C" {
|
||||||
let c1 = Point::new(num(&mut i)?, num(&mut i)?);
|
let c1 = Point::new(num(&mut i)?, num(&mut i)?);
|
||||||
let c2 = Point::new(num(&mut i)?, num(&mut i)?);
|
let c2 = Point::new(num(&mut i)?, num(&mut i)?);
|
||||||
|
|
@ -113,7 +115,9 @@ pub fn parse(src: &str) -> std::result::Result<Path, String> {
|
||||||
p,
|
p,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
s.knots.last_mut().expect("non-empty").out_ctrl = c1;
|
if let Some(k) = s.knots.last_mut() {
|
||||||
|
k.out_ctrl = c1;
|
||||||
|
}
|
||||||
s.knots.push(Knot { anchor: p, in_ctrl: c2, out_ctrl: p, smooth: false });
|
s.knots.push(Knot { anchor: p, in_ctrl: c2, out_ctrl: p, smooth: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,14 @@ fn kind_json(k: &VarKind) -> Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `a` with `b`'s fields added (both are JSON objects).
|
||||||
|
fn merged(mut a: Value, b: Value) -> Value {
|
||||||
|
if let (Some(ao), Value::Object(bo)) = (a.as_object_mut(), b) {
|
||||||
|
ao.extend(bo);
|
||||||
|
}
|
||||||
|
a
|
||||||
|
}
|
||||||
|
|
||||||
fn value_json(v: &VarValue) -> Value {
|
fn value_json(v: &VarValue) -> Value {
|
||||||
match v {
|
match v {
|
||||||
VarValue::Visibility(b) => json!({"kind": "visibility", "value": b}),
|
VarValue::Visibility(b) => json!({"kind": "visibility", "value": b}),
|
||||||
|
|
@ -355,16 +363,12 @@ fn list(s: &mut Session) -> Result<Value> {
|
||||||
let v = &s.active().ok_or(EngineError::NoDocument)?.doc.variables;
|
let v = &s.active().ok_or(EngineError::NoDocument)?.doc.variables;
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"defs": v.defs.iter().map(|d| {
|
"defs": v.defs.iter().map(|d| {
|
||||||
let mut o = json!({"name": d.name, "layer": d.layer.0});
|
merged(json!({"name": d.name, "layer": d.layer.0}), kind_json(&d.kind))
|
||||||
o.as_object_mut().unwrap().extend(kind_json(&d.kind).as_object().unwrap().clone());
|
|
||||||
o
|
|
||||||
}).collect::<Vec<_>>(),
|
}).collect::<Vec<_>>(),
|
||||||
"dataSets": v.data_sets.iter().map(|ds| json!({
|
"dataSets": v.data_sets.iter().map(|ds| json!({
|
||||||
"name": ds.name,
|
"name": ds.name,
|
||||||
"values": ds.values.iter().map(|dv| {
|
"values": ds.values.iter().map(|dv| {
|
||||||
let mut o = json!({"variable": dv.variable});
|
merged(json!({"variable": dv.variable}), value_json(&dv.value))
|
||||||
o.as_object_mut().unwrap().extend(value_json(&dv.value).as_object().unwrap().clone());
|
|
||||||
o
|
|
||||||
}).collect::<Vec<_>>(),
|
}).collect::<Vec<_>>(),
|
||||||
})).collect::<Vec<_>>(),
|
})).collect::<Vec<_>>(),
|
||||||
"active": v.active,
|
"active": v.active,
|
||||||
|
|
|
||||||
|
|
@ -439,7 +439,9 @@ fn with_shape<R>(s: &mut Session, id: LayerId, label: &str, f: impl FnOnce(&mut
|
||||||
if !matches!(l.content, LayerContent::Shape(_)) {
|
if !matches!(l.content, LayerContent::Shape(_)) {
|
||||||
return Err(EngineError::Other(format!("layer {} is a {} layer, not a shape layer", id.0, l.content.kind_name())));
|
return Err(EngineError::Other(format!("layer {} is a {} layer, not a shape layer", id.0, l.content.kind_name())));
|
||||||
}
|
}
|
||||||
let LayerContent::Shape(mut sh) = std::mem::replace(&mut l.content, LayerContent::Fill(Fill::Solid(Color::BLACK))) else { unreachable!() };
|
let LayerContent::Shape(mut sh) = std::mem::replace(&mut l.content, LayerContent::Fill(Fill::Solid(Color::BLACK))) else {
|
||||||
|
return Err(EngineError::Other(format!("layer {} is not a shape layer", id.0)));
|
||||||
|
};
|
||||||
let r = f(&mut sh, l);
|
let r = f(&mut sh, l);
|
||||||
refresh_shape(&snapshot, &mut sh);
|
refresh_shape(&snapshot, &mut sh);
|
||||||
l.content = LayerContent::Shape(sh);
|
l.content = LayerContent::Shape(sh);
|
||||||
|
|
|
||||||
|
|
@ -618,8 +618,9 @@ fn quantize_rows(px: &mut [[f32; 4]], w: usize, pal: &[[u8; 3]], dither: Dither,
|
||||||
.for_each(|(i, (p, out))| out.copy_from_slice(&quantize_band(p, w, i * 64, pal, dither, amount, t)));
|
.for_each(|(i, (p, out))| out.copy_from_slice(&quantize_band(p, w, i * 64, pal, dither, amount, t)));
|
||||||
idx
|
idx
|
||||||
}
|
}
|
||||||
|
// Single-threaded on wasm (the early return above always takes this path there).
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
unreachable!()
|
quantize_band(px, w, 0, pal, dither, amount, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A file-name-safe slice name.
|
/// A file-name-safe slice name.
|
||||||
|
|
@ -840,8 +841,19 @@ fn quick_export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let metadata = serde_json::to_value(prefs.metadata).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default();
|
let metadata = serde_json::to_value(prefs.metadata).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default();
|
||||||
let mut q = json!({"metadata": if metadata == "all" { "all" } else if metadata == "none" { "none" } else { "copyright" }, "convertToSrgb": prefs.convert_to_srgb, "transparency": true});
|
let mut o = serde_json::Map::new();
|
||||||
let o = q.as_object_mut().unwrap_or_else(|| unreachable!());
|
o.insert(
|
||||||
|
"metadata".into(),
|
||||||
|
json!(if metadata == "all" {
|
||||||
|
"all"
|
||||||
|
} else if metadata == "none" {
|
||||||
|
"none"
|
||||||
|
} else {
|
||||||
|
"copyright"
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
o.insert("convertToSrgb".into(), json!(prefs.convert_to_srgb));
|
||||||
|
o.insert("transparency".into(), json!(true));
|
||||||
match fmt.as_str() {
|
match fmt.as_str() {
|
||||||
"jpg" => {
|
"jpg" => {
|
||||||
o.insert("format".into(), json!("jpeg"));
|
o.insert("format".into(), json!("jpeg"));
|
||||||
|
|
@ -861,6 +873,7 @@ fn quick_export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
o.insert("format".into(), json!("png24"));
|
o.insert("format".into(), json!("png24"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let q = Value::Object(o);
|
||||||
let st = WebSettings::from_params(&q, cmd)?;
|
let st = WebSettings::from_params(&q, cmd)?;
|
||||||
let (wdoc, _, _) = web_document(&doc, &json!({}), &st)?;
|
let (wdoc, _, _) = web_document(&doc, &json!({}), &st)?;
|
||||||
let buf = photocraft_compose::flatten(&wdoc);
|
let buf = photocraft_compose::flatten(&wdoc);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue