mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-28 04:06:54 -06:00
This commit is contained in:
@@ -128,6 +128,8 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-10-13** · Control-channel clicks and drags validate positive counts and the complete pending-event budget before queuing input (#732). Oversized requests leave pending gestures intact, preventing unbounded click-count allocation. No readiness percentage change.
|
||||
- **2026-10-13** · Switching between document and parent pages refreshes the canvas immediately, preventing black parent pages and stale document objects when the zoom stays the same. Ordinary edits retain partial repainting. No readiness percentage change.
|
||||
- **2026-10-13** · Commands refuse parameter keys their params description doesn't name (#776): the control channel, MCP (`-32602`, every `batch` step checked before the first runs) and `designcraft-cli --cmd` answer with an “unknown parameter” error naming the nearest or the accepted keys instead of running with defaults; journals still replay. Descriptions of the table, anchored-object, data-merge and placing commands now name every key they read. No readiness percentage change.
|
||||
- **2026-10-13** · File ▸ Open Recent lists the documents last opened or saved (newest first, no duplicates; Save a Copy and exports don't count), with Clear Recent Files; the start screen shows the last eight, below the community card, as cards with a picture of the first page: saving puts one in the file (`preview.png`), so the start screen reads it without opening the document. A file that is gone is dimmed and says so when chosen, and stays listed. Preferences ▸ File Handling sets how many are kept (default 20, 0–100). `file.openRecent {index}`, `file.recentFiles`, `file.clearRecent` (#706). No readiness percentage change.
|
||||
- **2026-10-13** · Dropping an `.ase` file on the window loads its swatches, as Load Swatches does (the drop failed with "Can't Place the File"). No readiness percentage change.
|
||||
|
||||
@@ -19,15 +19,17 @@ pub fn has_xrefs(st: &Story) -> bool {
|
||||
}
|
||||
|
||||
impl XrefIndex {
|
||||
/// `get` composes a story (normally through the cache). Stories that themselves hold
|
||||
/// cross-references are composed without resolving them (their anchors' pages hardly move).
|
||||
/// `get` composes a story (normally through the cache). Stories that hold cross-references or
|
||||
/// running headers are composed without resolving them (their anchors' pages hardly move):
|
||||
/// through the cache they would build this index again, directly or by way of the running-header index.
|
||||
pub fn build(doc: &Document, get: &dyn Fn(StoryId) -> Arc<ComposedStory>) -> XrefIndex {
|
||||
let mut pages = HashMap::new();
|
||||
for (sid, st) in &doc.stories {
|
||||
if st.anchors.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let cs = if has_xrefs(st) { Arc::new(crate::compose_story(doc, *sid, &Default::default())) } else { get(*sid) };
|
||||
let indexed = has_xrefs(st) || crate::vars::has_running(doc, st);
|
||||
let cs = if indexed { Arc::new(crate::compose_story(doc, *sid, &Default::default())) } else { get(*sid) };
|
||||
for a in &st.anchors {
|
||||
let Some(pos) = st.anchor_pos(a.id) else { continue };
|
||||
if let Some((page, ..)) = crate::vars::place(doc, &cs, pos) {
|
||||
|
||||
@@ -214,4 +214,23 @@ mod tests {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.execute("xref.list", &json!({})).unwrap()[0]["text"], "(p. 3)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_reference_into_a_story_with_a_running_header_composes() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"pages": 1, "facingPages": false})).unwrap();
|
||||
let head = s.execute("frame.create", &json!({"rect": [72, 72, 400, 120], "content": "text", "text": "Chapter"})).unwrap()["story"].clone();
|
||||
let body = s.execute("frame.create", &json!({"rect": [72, 200, 400, 300], "content": "text", "text": "See"})).unwrap()["story"].clone();
|
||||
s.execute("text.select", &json!({"story": head, "anchor": 0, "focus": 0})).unwrap();
|
||||
s.execute("variables.insert", &json!({"name": "Running Header"})).unwrap();
|
||||
s.execute("text.select", &json!({"story": body, "anchor": 3, "focus": 3})).unwrap();
|
||||
s.execute("xref.insert", &json!({"story": head, "para": 0})).unwrap();
|
||||
// The cross-reference index and the running-header index used to build each other without end.
|
||||
assert_eq!(s.execute("xref.list", &json!({})).unwrap()[0]["text"], "\u{201C}Chapter\u{201D} on page 1");
|
||||
// The header shows the body's paragraph in front of its own text.
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let cs = s.cache.get(&d, StoryId(head.as_u64().unwrap()), None);
|
||||
let glyphs: usize = cs.frames[0].lines.iter().map(|l| l.glyphs.iter().filter(|g| g.visible && g.adv > 0.0).count()).sum();
|
||||
assert_eq!(glyphs, "SeeChapter".len());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ fn main() {
|
||||
}
|
||||
let mut harness = egui_kittest::Harness::builder()
|
||||
.with_size(egui::vec2(1440.0, 900.0))
|
||||
.with_step_dt(1.0 / 60.0)
|
||||
.with_pixels_per_point(2.0)
|
||||
.with_max_steps(1_000_000)
|
||||
.wgpu()
|
||||
|
||||
@@ -640,12 +640,14 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf,
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let ppp = ctx.pixels_per_point() as f64;
|
||||
let doc_key = (st.uid, std::sync::Arc::as_ptr(&st.doc) as u64, st.revision, preview);
|
||||
let editing_parents = st.editing_parents;
|
||||
// Overscan: 25% of the viewport on every side, within the GPU's maximum texture size.
|
||||
let max_side = ctx.input(|i| i.max_texture_side) as f32 / ppp as f32;
|
||||
let (ow, oh) =
|
||||
((rect.width() * 0.25).min((max_side - rect.width()) / 2.0).max(0.0), (rect.height() * 0.25).min((max_side - rect.height()) / 2.0).max(0.0));
|
||||
let want = |origin: Point| crate::Shown {
|
||||
doc: doc_key,
|
||||
editing_parents,
|
||||
origin: Point::new(origin.x - ow as f64 / xf.zoom, origin.y - oh as f64 / xf.zoom),
|
||||
zoom: xf.zoom,
|
||||
size: (rect.width() + 2.0 * ow, rect.height() + 2.0 * oh),
|
||||
@@ -653,7 +655,7 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf,
|
||||
let target = want(xf.origin);
|
||||
// Does the current texture cover the viewport at this zoom and document state?
|
||||
let covers = |s: &crate::Shown| {
|
||||
s.doc == doc_key && (s.zoom - xf.zoom).abs() < 1e-9 && {
|
||||
s.doc == doc_key && s.editing_parents == editing_parents && (s.zoom - xf.zoom).abs() < 1e-9 && {
|
||||
let x0 = (xf.origin.x - s.origin.x) * s.zoom;
|
||||
let y0 = (xf.origin.y - s.origin.y) * s.zoom;
|
||||
x0 >= -0.5 && y0 >= -0.5 && x0 + rect.width() as f64 <= s.size.0 as f64 + 0.5 && y0 + rect.height() as f64 <= s.size.1 as f64 + 0.5
|
||||
@@ -675,6 +677,7 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf,
|
||||
return;
|
||||
}
|
||||
let doc_changed = app.canvas.shown.is_none_or(|s| s.doc != doc_key);
|
||||
let mode_changed = app.canvas.shown.is_some_and(|s| s.editing_parents != editing_parents);
|
||||
let zoom_same = app.canvas.shown.is_some_and(|s| (s.zoom - xf.zoom).abs() < 1e-9);
|
||||
let job = |app: &DesignApp| {
|
||||
let st = app.session.active()?;
|
||||
@@ -700,7 +703,9 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf,
|
||||
if doc_changed && zoom_same && patch_texture(app, layout, ppp, doc_key, preview) {
|
||||
return;
|
||||
}
|
||||
if !has_worker || app.canvas.shown.is_none() || (doc_changed && zoom_same) {
|
||||
// A mode switch changes every spread's meaning, even if the document has no damage.
|
||||
// Show the new canvas immediately, including when fitting it changes the zoom.
|
||||
if !has_worker || app.canvas.shown.is_none() || mode_changed || (doc_changed && zoom_same) {
|
||||
let t0 = crate::now_ms();
|
||||
let Some((doc, placed, w, h, view, opts)) = job(app) else { return };
|
||||
let img = app.canvas.renderer.render(&doc, &app.session.cache, &placed, w, h, view, &opts);
|
||||
@@ -708,6 +713,7 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf,
|
||||
app.canvas.shown = Some(target);
|
||||
app.canvas.shown_doc = Some(doc);
|
||||
app.canvas.pending = None;
|
||||
app.canvas.pending_doc = None;
|
||||
app.perf.render_ms = crate::now_ms() - t0;
|
||||
return;
|
||||
}
|
||||
@@ -715,7 +721,7 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf,
|
||||
{
|
||||
// Already rendering this view?
|
||||
if app.canvas.pending.is_some_and(|(_, p)| {
|
||||
p.doc == doc_key && (p.zoom - xf.zoom).abs() < 1e-9 && {
|
||||
p.doc == doc_key && p.editing_parents == editing_parents && (p.zoom - xf.zoom).abs() < 1e-9 && {
|
||||
let x0 = (xf.origin.x - p.origin.x) * p.zoom;
|
||||
let y0 = (xf.origin.y - p.origin.y) * p.zoom;
|
||||
x0 >= 0.0 && y0 >= 0.0 && x0 + rect.width() as f64 <= p.size.0 as f64 && y0 + rect.height() as f64 <= p.size.1 as f64
|
||||
@@ -738,7 +744,7 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf,
|
||||
/// Returns false when a full render is needed (view moved, too much changed, or damage unknown).
|
||||
fn patch_texture(app: &mut DesignApp, layout: &CanvasLayout, ppp: f64, doc_key: (u64, u64, u64, bool), preview: bool) -> bool {
|
||||
let (Some(sh), Some(old), Some(st)) = (app.canvas.shown, app.canvas.shown_doc.clone(), app.session.active()) else { return false };
|
||||
if sh.doc.0 != doc_key.0 || sh.doc.3 != doc_key.3 {
|
||||
if sh.doc.0 != doc_key.0 || sh.doc.3 != doc_key.3 || sh.editing_parents != st.editing_parents {
|
||||
return false;
|
||||
}
|
||||
let (tw, th) = ((sh.size.0 as f64 * ppp).round() as i64, (sh.size.1 as f64 * ppp).round() as i64);
|
||||
@@ -825,6 +831,17 @@ fn dashed(painter: &egui::Painter, a: Pos2, b: Pos2, stroke: Stroke, dash: f32,
|
||||
painter.extend(egui::Shape::dashed_line(&[a, b], stroke, dash, gap));
|
||||
}
|
||||
|
||||
/// The most baseline grid lines drawn on a page: one per point of the tallest page (216 in).
|
||||
const MAX_BASELINES: usize = 216 * 72;
|
||||
|
||||
/// Baseline grid lines inside a page, measured from its top: `start + n * increment` for n >= 0 (the bottom
|
||||
/// edge is not a line). Lines above the page are skipped without walking them, however far up the start is.
|
||||
fn baseline_grid_lines(start: f64, increment: f64, height: f64) -> impl Iterator<Item = f64> {
|
||||
let step = increment.max(1.0);
|
||||
let first = if start < 0.0 { start.rem_euclid(step) } else { start };
|
||||
(0..MAX_BASELINES).map(move |n| first + n as f64 * step).take_while(move |y| *y < height)
|
||||
}
|
||||
|
||||
fn draw_guides(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Document, layout: &CanvasLayout, t: &Tokens) {
|
||||
let s = &doc.settings;
|
||||
for slot in &layout.slots {
|
||||
@@ -854,13 +871,11 @@ fn draw_guides(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Document
|
||||
// Baseline grid.
|
||||
if app.ui.baseline_grid && xf.zoom >= s.baseline_grid.view_threshold {
|
||||
let g = &s.baseline_grid;
|
||||
let mut y = g.start;
|
||||
while y < p.height {
|
||||
for y in baseline_grid_lines(g.start, g.increment, p.height) {
|
||||
painter.line_segment(
|
||||
[pt(Point::new(pr.x0, pr.y0 + y)), pt(Point::new(pr.x1, pr.y0 + y))],
|
||||
Stroke::new(hair(painter), c32(g.color).gamma_multiply(0.8)),
|
||||
);
|
||||
y += g.increment.max(1.0);
|
||||
}
|
||||
}
|
||||
// Margins (magenta) and columns (violet).
|
||||
@@ -2635,6 +2650,10 @@ fn release_clicks(press: Clicks, pos: Pos2, now: f64, dist: f32) -> Clicks {
|
||||
Clicks { at: now, pos, n }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "parent_canvas_tests.rs"]
|
||||
mod parent_canvas_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use designcraft_geom::Unit;
|
||||
@@ -2675,7 +2694,7 @@ mod tests {
|
||||
#[test]
|
||||
fn display_option_change_drops_the_render_in_flight() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
let shown = crate::Shown { doc: (1, 2, 3, false), origin: Point::new(0.0, 0.0), zoom: 1.0, size: (10.0, 10.0) };
|
||||
let shown = crate::Shown { doc: (1, 2, 3, false), origin: Point::new(0.0, 0.0), zoom: 1.0, size: (10.0, 10.0), editing_parents: false };
|
||||
invalidate_on_display_change(&mut app);
|
||||
app.canvas.shown = Some(shown);
|
||||
app.canvas.pending = Some((7, shown));
|
||||
@@ -4039,4 +4058,18 @@ mod scrollbar_tests {
|
||||
drag(&mut h, from, from + vec2(60.0, 40.0), Modifiers::NONE);
|
||||
assert_eq!(origin(&h), after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn baseline_grid_lines_are_the_ones_on_the_page() {
|
||||
// A start far above the page used to be walked line by line, without end.
|
||||
for start in [-1e9, -1e17] {
|
||||
let mut far = baseline_grid_lines(start, 12.0, 792.0);
|
||||
assert_eq!((far.next(), far.count()), (Some(8.0), 65));
|
||||
}
|
||||
let lines = |start, increment, height| baseline_grid_lines(start, increment, height).collect::<Vec<_>>();
|
||||
assert_eq!(lines(36.0, 12.0, 72.0), [36.0, 48.0, 60.0], "the bottom edge is not a line");
|
||||
assert_eq!(lines(-30.0, 12.0, 30.0), [6.0, 18.0], "a start above the page keeps its rhythm");
|
||||
assert_eq!(lines(f64::NAN, 12.0, 792.0), [0.0; 0]);
|
||||
assert_eq!(baseline_grid_lines(0.0, 12.0, f64::INFINITY).count(), MAX_BASELINES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,27 @@ fn key_from(name: &str) -> Option<egui::Key> {
|
||||
})
|
||||
}
|
||||
|
||||
// Retain room for the existing maximum drag (10,000 moves plus move/down/up), but
|
||||
// bound the whole gesture and already queued input before generating any events.
|
||||
const MAX_PENDING_POINTER_EVENTS: usize = 10_003;
|
||||
|
||||
fn pointer_counts(p: &Value, drag: bool, pending: usize) -> Result<(u64, u64), String> {
|
||||
let positive = |name: &str, default: u64| match p.get(name) {
|
||||
None => Ok(default),
|
||||
Some(v) => v.as_u64().filter(|n| *n > 0).ok_or_else(|| format!("`{name}` must be a positive integer")),
|
||||
};
|
||||
let count = positive("count", 1)?;
|
||||
let steps = if drag { positive("steps", 8)? } else { 0 };
|
||||
// One initial move, two button events per click, and each drag step. Check the
|
||||
// arithmetic too: hostile counts must never overflow into a small budget.
|
||||
let events = count.checked_mul(2).and_then(|n| n.checked_add(steps)).and_then(|n| n.checked_add(1));
|
||||
let available = MAX_PENDING_POINTER_EVENTS.saturating_sub(pending) as u64;
|
||||
if events.is_none_or(|n| n > available) {
|
||||
return Err(format!("pointer input would exceed the {MAX_PENDING_POINTER_EVENTS} pending-event limit"));
|
||||
}
|
||||
Ok((count, steps))
|
||||
}
|
||||
|
||||
/// Handle one control request. Commands it runs leave the user's system clipboard alone.
|
||||
pub fn handle(app: &mut DesignApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome {
|
||||
let was = std::mem::replace(&mut app.in_control, true);
|
||||
@@ -221,6 +242,10 @@ fn handle_request(app: &mut DesignApp, ctx: &egui::Context, req: &ControlRequest
|
||||
}
|
||||
"ui.click" | "ui.drag" => {
|
||||
// Screen-space (egui points) pointer input through egui itself: reaches every widget.
|
||||
let (count, steps) = match pointer_counts(p, req.method == "ui.drag", app.synthetic.len()) {
|
||||
Ok(counts) => counts,
|
||||
Err(e) => return err(e),
|
||||
};
|
||||
let f = |k: &str| p.get(k).and_then(Value::as_f64).unwrap_or(0.0) as f32;
|
||||
let button = match s("button") {
|
||||
Some("right") | Some("secondary") => egui::PointerButton::Secondary,
|
||||
@@ -240,15 +265,12 @@ fn handle_request(app: &mut DesignApp, ctx: &egui::Context, req: &ControlRequest
|
||||
app.synthetic.push(egui::Event::PointerMoved(a));
|
||||
app.synthetic.push(egui::Event::PointerButton { pos: a, button, pressed: true, modifiers });
|
||||
if req.method == "ui.drag" {
|
||||
// From the caller: a huge count would queue events until memory runs out.
|
||||
let steps = p.get("steps").and_then(Value::as_u64).unwrap_or(8).clamp(1, 10_000);
|
||||
for i in 1..=steps {
|
||||
let t = i as f32 / steps as f32;
|
||||
app.synthetic.push(egui::Event::PointerMoved(a + (end - a) * t));
|
||||
}
|
||||
}
|
||||
app.synthetic.push(egui::Event::PointerButton { pos: end, button, pressed: false, modifiers });
|
||||
let count = p.get("count").and_then(Value::as_u64).unwrap_or(1);
|
||||
for _ in 1..count {
|
||||
app.synthetic.push(egui::Event::PointerButton { pos: end, button, pressed: true, modifiers });
|
||||
app.synthetic.push(egui::Event::PointerButton { pos: end, button, pressed: false, modifiers });
|
||||
@@ -474,6 +496,127 @@ mod automation_contract_tests {
|
||||
output.textures_delta.clear();
|
||||
}
|
||||
#[test]
|
||||
fn oversized_pointer_requests_leave_pending_events_unchanged() {
|
||||
let mut app = app();
|
||||
let ctx = egui::Context::default();
|
||||
let pending = vec![egui::Event::PointerMoved(egui::pos2(12.0, 34.0))];
|
||||
for (method, params) in [
|
||||
("ui.click", json!({"count": 5002})),
|
||||
("ui.drag", json!({"steps": 10000, "count": 2})),
|
||||
("ui.drag", json!({"steps": 10001})),
|
||||
("ui.click", json!({"count": u64::MAX})),
|
||||
("ui.drag", json!({"steps": u64::MAX})),
|
||||
("ui.drag", json!({"count": u64::MAX, "steps": u64::MAX})),
|
||||
] {
|
||||
app.synthetic = pending.clone();
|
||||
let reply = call(&mut app, &ctx, method, params);
|
||||
assert_eq!(reply["ok"], false, "{method}: {reply}");
|
||||
assert_eq!(app.synthetic, pending, "a rejected gesture must not leave a press queued");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn pointer_counts_require_positive_json_integers() {
|
||||
let mut app = app();
|
||||
let ctx = egui::Context::default();
|
||||
let pending = vec![egui::Event::PointerMoved(egui::pos2(12.0, 34.0))];
|
||||
app.synthetic = pending.clone();
|
||||
for value in [json!(0), json!(-1), json!(1.5), Value::Null, json!("2"), json!(true), json!(18446744073709551616_f64)] {
|
||||
for (method, field) in [("ui.click", "count"), ("ui.drag", "count"), ("ui.drag", "steps")] {
|
||||
let mut params = json!({});
|
||||
params[field] = value.clone();
|
||||
let reply = call(&mut app, &ctx, method, params);
|
||||
assert_eq!(reply["ok"], false, "{method}, {field}={value}: {reply}");
|
||||
assert!(reply["error"].as_str().unwrap().contains(field));
|
||||
assert_eq!(app.synthetic, pending);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn pointer_gestures_accept_the_exact_event_budget() {
|
||||
let mut app = app();
|
||||
let ctx = egui::Context::default();
|
||||
for (method, params) in
|
||||
[("ui.click", json!({"count": 5001})), ("ui.drag", json!({"steps": 10000})), ("ui.drag", json!({"steps": 9998, "count": 2}))]
|
||||
{
|
||||
app.synthetic.clear();
|
||||
assert_eq!(call(&mut app, &ctx, method, params)["ok"], true, "{method}");
|
||||
assert_eq!(app.synthetic.len(), 10003);
|
||||
assert!(matches!(app.synthetic.last(), Some(egui::Event::PointerButton { pressed: false, .. })));
|
||||
let queued = app.synthetic.clone();
|
||||
assert_eq!(call(&mut app, &ctx, "ui.click", json!({}))["ok"], false);
|
||||
assert_eq!(app.synthetic, queued, "a full queue retains its release events");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn pointer_budget_counts_pending_input_and_recovers_when_it_drains() {
|
||||
let mut app = app();
|
||||
let ctx = egui::Context::default();
|
||||
assert_eq!(call(&mut app, &ctx, "ui.click", json!({"count": 5000}))["ok"], true);
|
||||
assert_eq!(app.synthetic.len(), 10001);
|
||||
let queued = app.synthetic.clone();
|
||||
assert_eq!(call(&mut app, &ctx, "ui.click", json!({}))["ok"], false);
|
||||
assert_eq!(app.synthetic, queued);
|
||||
// The ordinary input hook consumes one pointer event per frame, freeing
|
||||
// enough room for a complete click without dropping earlier input.
|
||||
app.raw_input_hook(&mut egui::RawInput::default());
|
||||
assert_eq!(app.synthetic.len(), 10000);
|
||||
assert_eq!(call(&mut app, &ctx, "ui.click", json!({}))["ok"], true);
|
||||
assert_eq!(app.synthetic.len(), 10003);
|
||||
|
||||
app.synthetic.clear();
|
||||
assert_eq!(call(&mut app, &ctx, "ui.move", json!({"x": 12, "y": 34}))["ok"], true);
|
||||
let queued = app.synthetic.clone();
|
||||
assert_eq!(call(&mut app, &ctx, "ui.drag", json!({"steps": 10000}))["ok"], false);
|
||||
assert_eq!(app.synthetic, queued);
|
||||
assert_eq!(call(&mut app, &ctx, "ui.drag", json!({"steps": 9999}))["ok"], true);
|
||||
assert_eq!(app.synthetic.len(), 10003);
|
||||
}
|
||||
#[test]
|
||||
fn ordinary_pointer_gestures_keep_their_defaults_and_event_order() {
|
||||
let mut app = app();
|
||||
let ctx = egui::Context::default();
|
||||
let a = egui::pos2(10.0, 20.0);
|
||||
let end = egui::pos2(30.0, 40.0);
|
||||
assert_eq!(call(&mut app, &ctx, "ui.click", json!({"x": 10, "y": 20}))["ok"], true);
|
||||
assert_eq!(
|
||||
app.synthetic,
|
||||
vec![
|
||||
egui::Event::PointerMoved(a),
|
||||
egui::Event::PointerButton { pos: a, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE },
|
||||
egui::Event::PointerButton { pos: a, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE },
|
||||
]
|
||||
);
|
||||
app.synthetic.clear();
|
||||
assert_eq!(call(&mut app, &ctx, "ui.drag", json!({"x": 10, "y": 20, "toX": 30, "toY": 40}))["ok"], true);
|
||||
assert_eq!(app.synthetic.len(), 11, "default drag has eight steps");
|
||||
assert_eq!(app.synthetic.get(9), Some(&egui::Event::PointerMoved(end)));
|
||||
|
||||
app.synthetic.clear();
|
||||
assert_eq!(
|
||||
call(
|
||||
&mut app,
|
||||
&ctx,
|
||||
"ui.drag",
|
||||
json!({"x": 10, "y": 20, "toX": 30, "toY": 40, "steps": 2, "count": 2, "button": "right", "shift": true})
|
||||
)["ok"],
|
||||
true
|
||||
);
|
||||
let modifiers = egui::Modifiers { shift: true, ..Default::default() };
|
||||
let button = egui::PointerButton::Secondary;
|
||||
assert_eq!(
|
||||
app.synthetic,
|
||||
vec![
|
||||
egui::Event::PointerMoved(a),
|
||||
egui::Event::PointerButton { pos: a, button, pressed: true, modifiers },
|
||||
egui::Event::PointerMoved(egui::pos2(20.0, 30.0)),
|
||||
egui::Event::PointerMoved(end),
|
||||
egui::Event::PointerButton { pos: end, button, pressed: false, modifiers },
|
||||
egui::Event::PointerButton { pos: end, button, pressed: true, modifiers },
|
||||
egui::Event::PointerButton { pos: end, button, pressed: false, modifiers },
|
||||
]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn control_typing_uses_explicit_caret_but_retains_widget_events() {
|
||||
let mut app = app();
|
||||
let ctx = egui::Context::default();
|
||||
|
||||
@@ -2692,7 +2692,10 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
|
||||
let words: Vec<String> = d.s("userWords").lines().map(str::to_string).collect();
|
||||
app.run("spelling.setWords", json!({"words": words}))?;
|
||||
}
|
||||
app.ui.story_editor_size = d.n("storyEditorSize").unwrap_or(14.0) as f32;
|
||||
// The slider's range. A size that isn't finite is saved as `null`, and ui.json then fails to load.
|
||||
if let Some(size) = d.n("storyEditorSize").filter(|v| v.is_finite()) {
|
||||
app.ui.story_editor_size = size.clamp(8.0, 36.0) as f32;
|
||||
}
|
||||
// Highlight options change the screen view only.
|
||||
app.canvas.shown = None;
|
||||
let view = match d.s("displayQuality").as_str() {
|
||||
@@ -6294,6 +6297,22 @@ mod tests {
|
||||
assert_eq!([t.cell(0, 0).unwrap().strokes[2].weight, t.cell(1, 1).unwrap().strokes[0].weight], [1.0; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferences_ok_saves_a_story_editor_size_that_loads_back() {
|
||||
// `ui.dialog.set` takes any value; a size that isn't finite is saved as `null`.
|
||||
let sizes = [(json!("nan"), 20.0), (json!("inf"), 20.0), (json!(null), 20.0), (json!(1e39), 36.0), (json!(3), 8.0), (json!(11), 11.0)];
|
||||
for (size, want) in sizes {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
app.ui.story_editor_size = 20.0;
|
||||
app.run("app.preferences", json!({})).unwrap();
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("storyEditorSize".into(), size);
|
||||
confirm(&mut app).unwrap();
|
||||
let saved = serde_json::to_vec(&app.ui).unwrap();
|
||||
assert_eq!(crate::UiState::from_preferences_json(&saved).unwrap().story_editor_size, want);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferences_ok_sets_only_the_ruler_units_it_is_given() {
|
||||
// `ui.dialog.open` takes any fields, so the vertical units can be missing.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -55,7 +55,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Paragraph", "Akapit"),
|
||||
("Tabs", "Tabulatory"),
|
||||
("Glyphs", "Glify"),
|
||||
("Story", "Artykuł tekstowy"),
|
||||
("Story", "Wątek"),
|
||||
("Character Styles", "Style znakowe"),
|
||||
("Paragraph Styles", "Style akapitowe"),
|
||||
("Create Outlines", "Utwórz kontury"),
|
||||
@@ -350,7 +350,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("This PDF has no layers.", "Ten PDF nie ma warstw."),
|
||||
("To Text:", "Na tekst:"),
|
||||
("Top", "Góra"),
|
||||
("Track Changes in All Stories", "Śledź zmiany we wszystkich artykułach"),
|
||||
("Track Changes in All Stories", "Śledź zmiany we wszystkich wątkach"),
|
||||
("Trim", "Format netto"),
|
||||
("Type:", "Typ:"),
|
||||
("Typical", "Typowe"),
|
||||
@@ -504,8 +504,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Document…", "Dokument…"),
|
||||
("Dynamic Spelling", "Dynamiczne sprawdzanie pisowni"),
|
||||
("Edit Parents", "Edytuj strony wzorcowe"),
|
||||
("Edit Story", "Edytuj artykuł"),
|
||||
("Edit in Story Editor", "Edytuj w edytorze artykułów"),
|
||||
("Edit Story", "Edytuj wątek"),
|
||||
("Edit in Story Editor", "Edytuj w edytorze wątków"),
|
||||
("Endnotes", "Przypisy końcowe"),
|
||||
("Entire Pasteboard", "Cały stół montażowy"),
|
||||
("Erase", "Wymaż"),
|
||||
@@ -537,7 +537,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Generate Index…", "Generuj indeks…"),
|
||||
("Generate QR Code…", "Generuj kod QR…"),
|
||||
("Generate Static Caption", "Generuj statyczny podpis"),
|
||||
("Get Story", "Pobierz artykuł"),
|
||||
("Get Story", "Pobierz wątek"),
|
||||
("Get Table", "Pobierz tabelę"),
|
||||
("Global Light…", "Światło globalne…"),
|
||||
("Go To Link", "Przejdź do łącza"),
|
||||
@@ -570,7 +570,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Last Object Below", "Ostatni obiekt poniżej"),
|
||||
("Library Contents", "Zawartość biblioteki"),
|
||||
("Library Items", "Elementy biblioteki"),
|
||||
("Linked Stories", "Połączone artykuły"),
|
||||
("Linked Stories", "Połączone wątki"),
|
||||
("Liquid Page Rule", "Reguła płynnej strony"),
|
||||
("List Documents", "Wyświetl listę dokumentów"),
|
||||
("List Styles", "Wyświetl listę stylów"),
|
||||
@@ -812,7 +812,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Send to Back", "Przesuń na spód"),
|
||||
("Set Cross-Reference Format", "Ustaw format odsyłacza"),
|
||||
("Set Footnote Text", "Ustaw tekst przypisu"),
|
||||
("Set Story Text", "Ustaw tekst artykułu"),
|
||||
("Set Story Text", "Ustaw tekst wątku"),
|
||||
("Shear", "Pochyl"),
|
||||
("Show All on Spread", "Pokaż wszystko na rozkładówce"),
|
||||
("Show State", "Pokaż stan"),
|
||||
@@ -1292,7 +1292,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Russian", "Rosyjski"),
|
||||
("Solid", "Ciągła"),
|
||||
("Spanish", "Hiszpański"),
|
||||
("Story Editor", "Edytor artykułów"),
|
||||
("Story Editor", "Edytor wątków"),
|
||||
("Swedish", "Szwedzki"),
|
||||
("Thick - Thick", "Gruba - gruba"),
|
||||
("Thick - Thin", "Gruba - cienka"),
|
||||
@@ -1353,7 +1353,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Normal (W)", "Normalny (W)"),
|
||||
("objects", "Obiekty"),
|
||||
("pages", "Strony"),
|
||||
("stories", "Artykuły"),
|
||||
("stories", "Wątki"),
|
||||
("items", "Elementy"),
|
||||
("warnings", "Ostrzeżenia"),
|
||||
("Render", "Renderuj"),
|
||||
@@ -1368,7 +1368,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Absolute Numbering (instead of Section Numbering)", "Numeracja bezwzględna (zamiast numeracji sekcji)"),
|
||||
(
|
||||
"Add and remove pages as the primary text frame's story grows and shrinks",
|
||||
"Dodawaj i usuwaj strony, gdy artykuł głównej ramki tekstowej rośnie lub się kurczy",
|
||||
"Dodawaj i usuwaj strony, gdy wątek głównej ramki tekstowej rośnie lub się kurczy",
|
||||
),
|
||||
("Added Text (highlighted)", "Dodany tekst (wyróżniony)"),
|
||||
("Adjust Layout (objects follow the new page size)", "Dostosuj układ (obiekty dopasują się do nowego rozmiaru strony)"),
|
||||
@@ -1406,7 +1406,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Guides & Pasteboard", "Linie pomocnicze i stół montażowy"),
|
||||
("Dictionary", "Słownik"),
|
||||
("Autocorrect", "Autokorekta"),
|
||||
("Story Editor Display", "Wyświetlanie edytora artykułów"),
|
||||
("Story Editor Display", "Wyświetlanie edytora wątków"),
|
||||
("File Handling", "Obsługa plików"),
|
||||
("Formatting affects container (J)", "Formatowanie dotyczy kontenera (J)"),
|
||||
("Formatting affects text (J)", "Formatowanie dotyczy tekstu (J)"),
|
||||
@@ -1420,7 +1420,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Version", "Wersja"),
|
||||
("{count} characters don't fit", "Niemieszczące się znaki: {count}"),
|
||||
("document", "dokument"),
|
||||
("story", "artykuł"),
|
||||
("story", "wątek"),
|
||||
("selection", "zaznaczenie"),
|
||||
("top", "góra"),
|
||||
("center", "środek"),
|
||||
@@ -1696,7 +1696,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Sixth Space", "Szóstka firetu"),
|
||||
("Slug:", "Obszar informacyjny:"),
|
||||
("Smart Guide Options", "Opcje inteligentnych linii pomocniczych"),
|
||||
("Story Column Direction", "Kierunek łamów artykułu"),
|
||||
("Story Column Direction", "Kierunek łamów wątku"),
|
||||
("Straight Double Quotation Marks", "Proste cudzysłowy podwójne"),
|
||||
("Straight Single Quotation Mark (Apostrophe)", "Prosty cudzysłów pojedynczy (apostrof)"),
|
||||
("Subscript:", "Indeks dolny:"),
|
||||
|
||||
@@ -453,6 +453,8 @@ impl Default for View {
|
||||
pub struct Shown {
|
||||
/// (doc uid, doc pointer, revision, preview).
|
||||
pub doc: (u64, u64, u64, bool),
|
||||
/// Parent and document spreads have different canvas placement and content.
|
||||
pub editing_parents: bool,
|
||||
/// Canvas point at the texture's top-left, zoom, and texture size in screen points.
|
||||
pub origin: Point,
|
||||
pub zoom: f64,
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
use super::*;
|
||||
|
||||
fn app_with_document_object() -> DesignApp {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"width": 64, "height": 64, "pages": 1, "facingPages": true, "margins": 4})).unwrap();
|
||||
app.run("frame.create", json!({"rect": [24, 24, 40, 40], "content": "unassigned"})).unwrap();
|
||||
app.run("object.fill", json!({"swatch": "[Black]"})).unwrap();
|
||||
// Exercise the production texture cache without starting a background thread.
|
||||
app.canvas.worker_started = true;
|
||||
app
|
||||
}
|
||||
|
||||
fn render(app: &mut DesignApp, ctx: &egui::Context) -> egui::epaint::ImageDelta {
|
||||
render_at_zoom(app, ctx, 1.0)
|
||||
}
|
||||
|
||||
fn render_at_zoom(app: &mut DesignApp, ctx: &egui::Context, zoom: f64) -> egui::epaint::ImageDelta {
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(128.0, 64.0));
|
||||
let view = View { zoom, origin: Point::new(-64.0, 0.0), fitted: true, rotation: 0 };
|
||||
let mut output = ctx.run_ui(Default::default(), |ui| {
|
||||
let st = app.session.active().unwrap();
|
||||
let layout = CanvasLayout::new(&st.doc, st.editing_parents);
|
||||
render_texture(app, ui.ctx(), rect, &Xf::new(rect, &view), &layout, false);
|
||||
});
|
||||
let id = app.canvas.texture.as_ref().unwrap().id();
|
||||
let deltas = output.textures_delta.set.remove(&id);
|
||||
output.textures_delta.clear();
|
||||
let mut deltas = deltas.expect("canvas pixels must update");
|
||||
assert_eq!(deltas.len(), 1, "one canvas render should produce one texture update");
|
||||
deltas.pop().unwrap()
|
||||
}
|
||||
|
||||
fn pixel(delta: &egui::epaint::ImageDelta, shown: crate::Shown, point: Point) -> Color32 {
|
||||
let egui::ImageData::Color(image) = &delta.image;
|
||||
let offset = delta.pos.unwrap_or([0, 0]);
|
||||
let x = ((point.x - shown.origin.x) * shown.zoom).round() as usize - offset[0];
|
||||
let y = ((point.y - shown.origin.y) * shown.zoom).round() as usize - offset[1];
|
||||
image[(x, y)]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switching_parent_editing_repaints_pages_and_document_objects() {
|
||||
let mut app = app_with_document_object();
|
||||
let ctx = egui::Context::default();
|
||||
let left_page = Point::new(-32.0, 32.0);
|
||||
let document_object = Point::new(32.0, 32.0);
|
||||
let before = render(&mut app, &ctx);
|
||||
let shown = app.canvas.shown.unwrap();
|
||||
assert_eq!(pixel(&before, shown, left_page), Color32::TRANSPARENT);
|
||||
let document_color = pixel(&before, shown, document_object);
|
||||
assert_ne!(document_color, Color32::WHITE);
|
||||
assert_eq!(document_color.a(), 255);
|
||||
|
||||
let doc = app.session.active().unwrap().doc.clone();
|
||||
app.run("layout.parents.edit", json!({"on": true})).unwrap();
|
||||
assert!(std::sync::Arc::ptr_eq(&doc, &app.session.active().unwrap().doc), "switching canvas mode does not edit the document");
|
||||
let parent = render(&mut app, &ctx);
|
||||
assert!(parent.pos.is_none(), "the parent canvas needs a complete repaint");
|
||||
let shown = app.canvas.shown.unwrap();
|
||||
assert_eq!(pixel(&parent, shown, left_page), Color32::WHITE, "the parent's left page must become visible");
|
||||
assert_eq!(pixel(&parent, shown, document_object), Color32::WHITE, "document-only objects must disappear in parent editing");
|
||||
|
||||
app.run("layout.parents.edit", json!({"on": false})).unwrap();
|
||||
let restored = render(&mut app, &ctx);
|
||||
assert!(restored.pos.is_none());
|
||||
let shown = app.canvas.shown.unwrap();
|
||||
assert_eq!(pixel(&restored, shown, left_page), Color32::TRANSPARENT);
|
||||
assert_eq!(pixel(&restored, shown, document_object), document_color);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn switching_parent_editing_with_a_new_zoom_repaints_before_the_worker() {
|
||||
let mut app = app_with_document_object();
|
||||
let ctx = egui::Context::default();
|
||||
app.canvas.worker = Some(crate::render_worker::Worker::spawn(ctx.clone()).expect("render worker must start"));
|
||||
let before = render(&mut app, &ctx);
|
||||
assert_ne!(pixel(&before, app.canvas.shown.unwrap(), Point::new(32.0, 32.0)), Color32::WHITE);
|
||||
|
||||
// Fitting the parent spread can change the zoom as well as the canvas mode.
|
||||
app.run("layout.parents.edit", json!({"on": true})).unwrap();
|
||||
let parent = render_at_zoom(&mut app, &ctx, 0.75);
|
||||
assert!(parent.pos.is_none(), "a mode switch must upload the new canvas in this frame");
|
||||
assert!(app.canvas.pending.is_none(), "parent pages must not wait behind an asynchronous zoom render");
|
||||
let shown = app.canvas.shown.unwrap();
|
||||
assert_eq!(shown.zoom, 0.75);
|
||||
assert_eq!(pixel(&parent, shown, Point::new(-32.0, 32.0)), Color32::WHITE);
|
||||
assert_eq!(pixel(&parent, shown, Point::new(32.0, 32.0)), Color32::WHITE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_edits_in_the_same_canvas_mode_still_repaint_partially() {
|
||||
let mut app = app_with_document_object();
|
||||
let ctx = egui::Context::default();
|
||||
let before = render(&mut app, &ctx);
|
||||
let point = Point::new(32.0, 32.0);
|
||||
assert_ne!(pixel(&before, app.canvas.shown.unwrap(), point), Color32::WHITE);
|
||||
|
||||
app.run("object.fill", json!({"swatch": "[Paper]"})).unwrap();
|
||||
let patch = render(&mut app, &ctx);
|
||||
assert!(patch.pos.is_some(), "an ordinary small edit should keep the partial repaint optimization");
|
||||
assert_eq!(app.canvas.patches, 1);
|
||||
assert_eq!(pixel(&patch, app.canvas.shown.unwrap(), point), Color32::WHITE);
|
||||
}
|
||||
@@ -42,7 +42,7 @@ so only enable it while you use it. Transport: `apps/designcraft/src/control_ser
|
||||
| `ui.key` / `ui.text` | `{key, shift?, alt?, cmd?}` / `{text}` | Synthetic keyboard input (typing into a text frame) |
|
||||
| `ui.ime` | `{preedit?, active?: [start, end], commit?}` | Input method events as the system IME sends them: marked text (shown in place, `active` is the clause being converted, in characters) and the committed text (typed as one `text.insert`) |
|
||||
| `ui.paste` | `{text}` | What Ctrl/Cmd+V delivers from the system clipboard: text into the text being edited, or SVG markup (vector art copied in Illustrator, VectorCraft or Figma) as editable paths in the middle of the view |
|
||||
| `ui.move` / `ui.click` / `ui.drag` | screen points, `button?: left\|right\|middle` | Real egui pointer input — reaches every widget, menu and panel |
|
||||
| `ui.move` / `ui.click` / `ui.drag` | screen points, `button?: left\|right\|middle`, `count?: positive integer` (click/drag, default 1), `steps?: positive integer` (drag, default 8) | Real egui pointer input — reaches every widget, menu and panel |
|
||||
| `ui.set` | `{brightness?, panel?, rulers?, guides?, frameEdges?, baselineGrid?, documentGrid?, textThreads?, screenMode?, zoom?, page?, fit?}` | UI state |
|
||||
| `ui.dialog.open` | `{id, fields?}` | Open a dialog by id (e.g. `paragraphStyleOptions` or `characterStyleOptions` with `{name, section}`, or `{new: true}` for a new style) |
|
||||
| `ui.dialog.set` / `ui.dialog.confirm` / `ui.dialog.cancel` | `{field, value}` | Fill and confirm the open dialog |
|
||||
@@ -60,6 +60,17 @@ Headless window screenshots (locked screen, hidden window): `cargo run -p design
|
||||
|
||||
The MCP server (`designcraft-cli mcp`) wraps the same methods for Claude and other agents.
|
||||
|
||||
### Click and drag limits
|
||||
|
||||
`ui.click` and `ui.drag` validate the whole gesture before queuing any input. They permit at
|
||||
most 10,003 pending synthetic events, including input already waiting for native frames. A
|
||||
click uses `1 + 2 * count` events; a drag uses `1 + steps + 2 * count`. The count includes the
|
||||
initial click or drag; further clicks occur at its endpoint. An empty queue therefore accepts
|
||||
up to 5,001 clicks, or a single drag with up to 10,000 steps. `count` and `steps` must be positive
|
||||
JSON integers when supplied; invalid or over-budget requests return `ok: false` and leave pending
|
||||
input unchanged. Oversized gestures are rejected rather than truncated. Wait for queued input
|
||||
to finish before retrying a request that would exceed the pending-event limit.
|
||||
|
||||
## Shared panel docking
|
||||
|
||||
Panel arrangements support tab grouping and reordering, horizontal and vertical splits,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# User docs: rules for agents and contributors
|
||||
|
||||
Everything in this folder (except this file, CLAUDE.md and README.md) is published at
|
||||
https://getartcraft.com/docs/designcraft. The file path is the URL:
|
||||
`troubleshooting/app-closes-at-start.md` → `/docs/designcraft/troubleshooting/app-closes-at-start`.
|
||||
Renaming a file breaks links, so add the old path to `redirect_from` when you rename.
|
||||
|
||||
## Who reads this
|
||||
|
||||
Artists and designers using DesignCraft, Discord mods answering them, and AI assistants looking things up for
|
||||
either. Write for an artist, not a developer.
|
||||
|
||||
## Page rules
|
||||
|
||||
- One question or problem per page. Lead with the answer or the fix in the first two lines.
|
||||
- Plain words, short sentences, numbered steps for anything the reader does.
|
||||
- Start the body at `##`. The page title comes from frontmatter.
|
||||
- Link to other pages instead of repeating them. Use relative links to the `.md` file:
|
||||
`[Install DesignCraft](../getting-started/install.md)`.
|
||||
- No marketing copy, no "simply", no claims you have not checked in the code or a release.
|
||||
- Call the product "DesignCraft", as the app, its downloads and getartcraft.com do.
|
||||
- Name UI labels exactly as the app shows them, in **bold**.
|
||||
- Document Windows 11 only. Do not name older Windows versions.
|
||||
- Troubleshooting pages: `## Symptoms`, `## Fix`, `## Why this happens` (optional),
|
||||
`## Still stuck?` (link the issue tracker or Discord).
|
||||
|
||||
## Frontmatter
|
||||
|
||||
---
|
||||
title: App closes right after it starts
|
||||
description: One sentence for search results, link previews and AI summaries.
|
||||
order: 10 # optional, sorts within the folder (lower first)
|
||||
verified: 0.6.0 # optional, app version you last checked this page against
|
||||
redirect_from: [old/path] # optional, old URLs (relative to /docs/designcraft)
|
||||
---
|
||||
|
||||
Only `key: value` and `key: [a, b]` lines work. Quote values that start with a quote or contain ` #`.
|
||||
|
||||
Set `verified` to the latest release (the newest release tag) after checking the page against that tag with `git show <tag>:<path>`. Pages describe the latest release users download, not unreleased code on main.
|
||||
|
||||
## Folders
|
||||
|
||||
- `getting-started/` install and first run
|
||||
- `guides/` how to do one thing
|
||||
- `troubleshooting/` symptom → fix
|
||||
- `faq/` short answers
|
||||
- `reference/` lists and settings
|
||||
|
||||
A folder's `index.md` names the folder in the sidebar. Files or folders starting with `_` are
|
||||
drafts and are not published.
|
||||
|
||||
## Check before you push
|
||||
|
||||
node <path to your artcraft-services checkout>/frontend/apps/artcraft-website-next/scripts/docs/core.mjs docs/user
|
||||
|
||||
The checker lives in storytold/artcraft-services. A CI check will be added once it is on that repo's main branch.
|
||||
|
||||
## Support PRs
|
||||
|
||||
Pages written to answer a user question come in as PRs titled `docs(support): <page title>`.
|
||||
Review them like code: check every claim against the source. Questions and bugs go to
|
||||
https://github.com/storytold/designcraft/issues.
|
||||
@@ -0,0 +1 @@
|
||||
Read `AGENTS.md` in this folder: it has the rules for the user docs.
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Can DesignCraft open InDesign files?
|
||||
description: DesignCraft opens and exports InDesign Markup (.idml) files. The Open dialog does not list InDesign's own .indd format.
|
||||
order: 30
|
||||
verified: 0.7.1
|
||||
---
|
||||
|
||||
Yes for IDML, the InDesign Markup format. DesignCraft can open `.idml` files and export them with **File**, then **Export IDML…**.
|
||||
|
||||
The **Open…** dialog lists `.designcraft` and `.idml` files only, so it does not offer InDesign's own `.indd` files. See [Open a DesignCraft or InDesign (IDML) file](../guides/open-a-file.md).
|
||||
|
||||
IDML import is not complete. Open issues in the tracker report that custom text-wrap boundaries ([#451](https://github.com/storytold/designcraft/issues/451)) and saved image clipping paths ([#450](https://github.com/storytold/designcraft/issues/450)) are lost when importing IDML, and that bullets and numbers in lists show as missing-font magenta in some templates ([#110](https://github.com/storytold/designcraft/issues/110)). Check your layout after importing.
|
||||
|
||||
DesignCraft can also export PDF (including PDF/X-4 and PDF/A-2b), PNG and EPUB. See [Export a PDF](../guides/export-a-pdf.md).
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: FAQ
|
||||
description: Short answers about DesignCraft's price, platforms and file formats.
|
||||
order: 40
|
||||
---
|
||||
|
||||
- [Is DesignCraft free?](is-it-free.md)
|
||||
- [Which systems does DesignCraft run on?](supported-systems.md)
|
||||
- [Can DesignCraft open InDesign files?](indesign-files.md)
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Is DesignCraft free?
|
||||
description: DesignCraft is free and open source, with no subscription, licence server or telemetry.
|
||||
order: 10
|
||||
verified: 0.7.1
|
||||
---
|
||||
|
||||
Yes. DesignCraft is free. It has no subscription, no licence server and no telemetry.
|
||||
|
||||
The code is open source under your choice of the [MIT](https://github.com/storytold/designcraft/blob/main/LICENSE-MIT) or [Apache-2.0](https://github.com/storytold/designcraft/blob/main/LICENSE-APACHE) licence. DesignCraft is in pre-alpha, so some InDesign features are not there yet. The list of known gaps is in [docs/gaps.md](https://github.com/storytold/designcraft/blob/main/docs/gaps.md) in the repository.
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Which systems does DesignCraft run on?
|
||||
description: DesignCraft runs on Windows 11, macOS and Linux, and in a web browser.
|
||||
order: 20
|
||||
verified: 0.7.1
|
||||
---
|
||||
|
||||
DesignCraft runs on Windows, macOS and Linux, and in a modern web browser. The release also has builds for FreeBSD.
|
||||
|
||||
- **Windows 11**: x64, arm64 and 32-bit x86 installers, plus portable zips.
|
||||
- **macOS**: one universal build for Apple silicon and Intel.
|
||||
- **Linux**: x86_64 and aarch64 as AppImage, Flatpak, `.deb`, `.rpm` or tarball, and RISC-V as `.deb`, `.rpm` or tarball.
|
||||
- **Web**: a static site you host yourself (`designcraft-web-<version>.zip`). It runs on WebGPU and falls back to WebGL2. The web build has no control channel, and Save and Export download the file.
|
||||
|
||||
All files are on the [latest release](https://github.com/storytold/designcraft/releases/latest). See [Install DesignCraft](../getting-started/install.md) for steps.
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Getting started
|
||||
description: Install DesignCraft, then open the sample magazine or start a new document.
|
||||
order: 10
|
||||
verified: 0.7.1
|
||||
---
|
||||
|
||||
Install DesignCraft first, then start a document.
|
||||
|
||||
1. [Install DesignCraft](install.md) on Windows 11, macOS or Linux.
|
||||
2. Open DesignCraft. The start screen has three buttons: **New file**, **Open** and **Open sample magazine**.
|
||||
3. Click **Open sample magazine** to see a finished layout, or follow [Create a new document](../guides/create-a-document.md) to begin your own.
|
||||
|
||||
DesignCraft also runs in a web browser. See [Which systems does DesignCraft run on?](../faq/supported-systems.md).
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Install DesignCraft
|
||||
description: Download DesignCraft 0.7.1 for Windows 11, macOS or Linux, or install it from ArtCraft Launcher.
|
||||
order: 20
|
||||
verified: 0.7.1
|
||||
---
|
||||
|
||||
Download the file for your system from the [latest release](https://github.com/storytold/designcraft/releases/latest) and install it. In the file names, `<version>` stands for the version number, for example `0.7.1`. The release also has `SHA256SUMS.txt`, which lists a checksum for every file. The [DesignCraft page on getartcraft.com](https://getartcraft.com/apps/designcraft) links to the download as well.
|
||||
|
||||
## Windows 11
|
||||
|
||||
1. Open the [latest release](https://github.com/storytold/designcraft/releases/latest).
|
||||
2. Download `designcraft-<version>-windows-x64.msi`. It fits most PCs (64-bit Intel and AMD).
|
||||
3. Run the file and follow the installer.
|
||||
4. Start **DesignCraft** from the Start menu.
|
||||
|
||||
The release also has an `arm64` installer for ARM PCs such as Snapdragon, and a 32-bit `x86` installer. To run without installing, download `designcraft-<version>-windows-x64-portable.zip` (or the `arm64` or `x86` version), unzip it and start the program inside. The installers and executables are code-signed.
|
||||
|
||||
## macOS
|
||||
|
||||
1. Download `designcraft-<version>-macos-universal.dmg`. It works on Apple silicon and Intel Macs.
|
||||
2. Open it and drag DesignCraft to Applications.
|
||||
|
||||
The app is signed and notarized.
|
||||
|
||||
## Linux
|
||||
|
||||
Pick one file from the release. Replace `x86_64` with `aarch64` on ARM.
|
||||
|
||||
| File | Notes |
|
||||
|-----------------------------------------------|----------------------------------------------------------|
|
||||
| `designcraft-<version>-linux-x86_64.AppImage` | Runs anywhere. |
|
||||
| `designcraft-<version>-linux-x86_64.flatpak` | Sandboxed. Install with `flatpak install --user <file>`. |
|
||||
| `designcraft-<version>-linux-x86_64.deb` | Debian and Ubuntu. |
|
||||
| `designcraft-<version>-linux-x86_64.rpm` | Fedora, RHEL and openSUSE. |
|
||||
| `designcraft-<version>-linux-x86_64.tar.gz` | Unpack anywhere. |
|
||||
|
||||
For RISC-V there is a `.deb`, an `.rpm` and a tarball, named with `riscv64` in place of `x86_64`. There is no AppImage or Flatpak for RISC-V. The RISC-V build needs glibc 2.39 or newer (Ubuntu 24.04 or newer). A FreeBSD build (`designcraft-<version>-freebsd-x86_64.tar.gz`) is on the release page too.
|
||||
|
||||
## Install with ArtCraft Launcher
|
||||
|
||||
ArtCraft Launcher lists DesignCraft and can install and update it for you. See [ArtCraft Launcher](https://getartcraft.com/docs/craft-launcher) for how it works.
|
||||
|
||||
## First run
|
||||
|
||||
Open DesignCraft. You land on the start screen. Continue with [Getting started](index.md). If the window closes right away, see [DesignCraft closes right after it starts](../troubleshooting/app-closes-at-start.md).
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: Create a new document
|
||||
description: Start a new DesignCraft document from a preset or with your own page size, margins and columns.
|
||||
order: 10
|
||||
verified: 0.7.1
|
||||
---
|
||||
|
||||
Choose **File**, then **New Document…** (Ctrl+N), set the page size and margins, and click **OK**.
|
||||
|
||||
Shortcuts on this page use Ctrl. On macOS, use Cmd instead of Ctrl.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Open the **File** menu and choose **New Document…**. On the start screen you can click **New file** instead.
|
||||
2. In the **New Document** dialog, pick a **Preset**, or leave it and type your own **Width** and **Height**.
|
||||
3. Set **Pages**, and tick **Facing Pages** if you want left and right pages side by side as spreads.
|
||||
4. Set **Columns** and the **Gutter** between them.
|
||||
5. Set the margins: **Top**, **Bottom**, **Inside** and **Outside**.
|
||||
6. Set **Bleed** if the document goes to print.
|
||||
7. Tick **Primary Text Frame** if you want a text frame that fills the margins on each page.
|
||||
8. Click **OK**.
|
||||
|
||||
## Presets
|
||||
|
||||
The **Preset** list has these page sizes:
|
||||
|
||||
- Print: Letter, Legal, Tabloid, Letter - Half, A3, A4, A5, A6, B5, Business Card, Postcard.
|
||||
- Web: Web 1920 × 1080, Web 1366 × 768, Web 1024 × 768.
|
||||
- Mobile: Mobile 1080 × 1920, Mobile 390 × 844, Tablet 1024 × 1366.
|
||||
|
||||
Choosing a print preset ticks **Facing Pages**. Web and mobile presets leave it off.
|
||||
|
||||
## Quick start from the start screen
|
||||
|
||||
The start screen also shows a tile for each print preset under **Start a new document**. Click a tile to make a document at that size with no dialog.
|
||||
|
||||
## See a finished layout
|
||||
|
||||
Click **Open sample magazine** on the start screen. It opens a magazine with a cover, styled pages, threaded columns, text wrap and swatches, which is a good way to see what DesignCraft can do.
|
||||
|
||||
## Next
|
||||
|
||||
Save your work with **File**, then **Save** (Ctrl+S). The first save asks where to put the file and suggests a name ending in `.designcraft`.
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Export a PDF
|
||||
description: Export your DesignCraft document as a PDF for desktop printing, a print shop, screen or archive.
|
||||
order: 40
|
||||
verified: 0.7.1
|
||||
---
|
||||
|
||||
Choose **File**, then **Export PDF…**, pick a **Preset**, click **OK** and name the file.
|
||||
|
||||
Shortcuts on this page use Ctrl. On macOS, use Cmd instead of Ctrl.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Open the **File** menu and choose **Export PDF…**.
|
||||
2. In the **Export PDF** dialog, pick a **Preset:** (see the table below).
|
||||
3. Under **General**, leave **Page Range:** as `all`, or type the pages you want, for example `1-3,5`. **Spreads** is a checkbox in the same section.
|
||||
4. Under **Compression**, tick or clear **Compress images**.
|
||||
5. Under **Marks and Bleeds**, tick **Include document bleed**, **Crop Marks**, **Bleed Marks** or **Page Information** as your printer asks. **Weight:** and **Offset:** set the marks.
|
||||
6. Under **Advanced**, pick a **Standard:** (**None**, **PDF/X-4** or **PDF/A-2b**) and decide on **Tagged PDF**.
|
||||
7. Click **OK**, then choose where to save. The file name starts as your document's title with `.pdf`.
|
||||
|
||||
## Presets
|
||||
|
||||
Picking a preset fills in the other settings. Changing any setting by hand switches the preset to **Custom**.
|
||||
|
||||
| Preset | Standard | Compress images | Crop marks | Bleed marks |
|
||||
|---------------------|----------|-----------------|------------|-------------|
|
||||
| Desktop Printing | None | off | off | off |
|
||||
| Commercial Printing | None | on | on | on |
|
||||
| Screen and Email | None | on | off | off |
|
||||
| PDF/X-4 | PDF/X-4 | off | on | on |
|
||||
| PDF/A-2b | PDF/A-2b | off | off | off |
|
||||
|
||||
**Screen and Email** also sets the transparency flattener to low resolution, which draws pages with transparency at 72 ppi. DesignCraft remembers your last settings for next time.
|
||||
|
||||
## Other exports
|
||||
|
||||
The **File** menu also has **Export Page as PNG…** (Ctrl+E), **Export IDML…**, **Export EPUB…**, **Export EPUB (Fixed Layout)…**, **Export PDF (Interactive)…**, **Export HTML…**, **Export XML…**, **Export Text…** and **Print Booklet…**.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Guides
|
||||
description: Step-by-step guides for the main DesignCraft tasks.
|
||||
order: 20
|
||||
---
|
||||
|
||||
- [Create a new document](create-a-document.md)
|
||||
- [Open a DesignCraft or InDesign (IDML) file](open-a-file.md)
|
||||
- [Place images, PDFs and text](place-images-and-text.md)
|
||||
- [Export a PDF](export-a-pdf.md)
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Open a DesignCraft or InDesign (IDML) file
|
||||
description: Open a .designcraft document or an InDesign Markup (.idml) file, and what to do about missing fonts and incomplete IDML packages.
|
||||
order: 20
|
||||
verified: 0.7.1
|
||||
---
|
||||
|
||||
Choose **File**, then **Open…** (Ctrl+O), and pick a `.designcraft` or `.idml` file. The file opens as a new document.
|
||||
|
||||
Shortcuts on this page use Ctrl. On macOS, use Cmd instead of Ctrl.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Open the **File** menu and choose **Open…**. On the start screen you can click **Open** instead.
|
||||
2. In the file dialog, pick a file. The dialog lists **DesignCraft or IDML** (both, listed first), **DesignCraft** (`.designcraft`) and **InDesign Markup (IDML)** (`.idml`).
|
||||
3. Confirm the dialog. The document opens as a new document.
|
||||
|
||||
## What the Open dialog accepts
|
||||
|
||||
The Open dialog lists `.designcraft` and `.idml` files only. To bring an image, a PDF or a text file into a document, use [Place](place-images-and-text.md) instead.
|
||||
|
||||
IDML is InDesign Markup, the exchange format InDesign can export. Linked images are read from next to the file or from its `Links` folder. If a `Document Fonts` folder sits beside the file, DesignCraft loads those fonts first.
|
||||
|
||||
## Missing fonts
|
||||
|
||||
Text that uses a font DesignCraft cannot find is highlighted in pink. This is the missing-font highlight. Put the font files in a `Document Fonts` folder beside the file, or install the fonts on your system, so DesignCraft can find them.
|
||||
|
||||
## Import warnings
|
||||
|
||||
If an IDML package is missing some of its parts, DesignCraft still opens it and shows an **Import Warnings** alert that names each missing part. The contents of those parts are not in the document, so check your layout.
|
||||
|
||||
If the package has none of its spreads, there is nothing to open. DesignCraft then shows a **Can't Open the File** alert that names the missing parts.
|
||||
|
||||
## Save it again
|
||||
|
||||
**File**, then **Save** (Ctrl+S) writes a `.designcraft` file. To hand the layout back to InDesign, use **File**, then **Export IDML…**.
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: Place images, PDFs and text
|
||||
description: Bring images, PDF pages, Word and plain text files into a DesignCraft document with File, Place.
|
||||
order: 30
|
||||
verified: 0.7.1
|
||||
---
|
||||
|
||||
Choose **File**, then **Place…** (Ctrl+D), pick a file, and DesignCraft puts it into your document.
|
||||
|
||||
Shortcuts on this page use Ctrl. On macOS, use Cmd instead of Ctrl.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Open the **File** menu and choose **Place…**.
|
||||
2. Pick a file. The dialog's **Graphics and text** filter covers the file types below.
|
||||
3. For an image, DesignCraft places it in the selected empty frame if there is one. The image fills the frame once and keeps its proportions. **Auto-Fit** stays off, so resizing the frame later crops the image instead of scaling it.
|
||||
4. For a text file, it goes into the insertion point, the selected frame, or a new frame.
|
||||
|
||||
## File types
|
||||
|
||||
| Kind | Extensions |
|
||||
|----------|---------------------------------------------------------------------------------|
|
||||
| Images | png, jpg, jpeg, gif, webp, tif, tiff, bmp, psd, svg |
|
||||
| Vector | pdf, ai, eps |
|
||||
| Text | txt, docx, rtf, md |
|
||||
| Table | xlsx (placed as a table) |
|
||||
| Layout | idml, designcraft (one page's objects, placed as a group) |
|
||||
| Media | mp4, m4v, mov, webm, mp3, m4a, wav, ogg (placed in a media frame) |
|
||||
|
||||
The same dialog has separate filters for **Graphics**, **Text (Word, RTF, plain, Excel)** and **Video and sound**.
|
||||
|
||||
## Place a page of a PDF
|
||||
|
||||
1. Choose **File**, then **Place…** and pick a PDF with more than one page.
|
||||
2. In the **Place PDF** dialog, type the page number in **Page**. The label shows the page count.
|
||||
3. Pick a box in **Crop to:**. The choices are **Crop**, **Trim**, **Bleed**, **Art** and **Media**.
|
||||
4. Click **OK**.
|
||||
|
||||
A PDF with one page is placed without this dialog.
|
||||
|
||||
## Place Word or RTF text with your styles
|
||||
|
||||
**File**, then **Place with Import Options…** (Ctrl+Shift+D) opens an **Import Options** dialog for `.docx` and `.rtf` files, where you can map the file's styles to your document's styles before the text lands. For other file types it places the file straight away.
|
||||
|
||||
## If a file cannot be placed
|
||||
|
||||
DesignCraft shows an alert when a file cannot be opened or placed. Ask in [Discord](https://discord.gg/artcraft) or see [Report a problem](../troubleshooting/get-help.md).
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: DesignCraft
|
||||
description: DesignCraft is a free, open-source page layout app for print and screen, an alternative to Adobe InDesign.
|
||||
verified: 0.7.1
|
||||
---
|
||||
|
||||
DesignCraft is a page layout and publishing app. It is an open-source alternative to Adobe InDesign, built from scratch by the ArtCraft team. If you know InDesign's tools, menus and panels, you will find them here.
|
||||
|
||||
It has spreads and parent pages, text frames that thread from one to the next, paragraph and character styles, swatches and text wrap. It opens and saves InDesign Markup (IDML) files and exports PDF, PNG and EPUB. It is free, with no subscription, licence server or telemetry. The project is in pre-alpha, so expect gaps.
|
||||
|
||||
## Start here
|
||||
|
||||
- [Getting started](getting-started/index.md)
|
||||
- [Install DesignCraft](getting-started/install.md)
|
||||
- [Guides](guides/index.md)
|
||||
- [Troubleshooting](troubleshooting/index.md)
|
||||
- [FAQ](faq/index.md)
|
||||
|
||||
## Get help
|
||||
|
||||
Ask in [Discord](https://discord.gg/artcraft) or open an issue in the [issue tracker](https://github.com/storytold/designcraft/issues). Say which version and which system you use.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: DesignCraft closes right after it starts
|
||||
description: On Windows, DesignCraft shows a brief spinner and then closes with no message. Update, or force the DirectX 12 graphics backend.
|
||||
order: 10
|
||||
verified: 0.7.1
|
||||
---
|
||||
|
||||
If DesignCraft flashes a spinner and closes, update to the [latest release](https://github.com/storytold/designcraft/releases/latest). If it still closes, set the environment variable `WGPU_BACKEND` to `dx12` and start it again.
|
||||
|
||||
## Symptoms
|
||||
|
||||
- On Windows, DesignCraft opens briefly with a spinner or a quick blue circle, then closes. No error message appears.
|
||||
- Windows may record a crash that names a graphics driver file such as `atio6axx.dll` (AMD) or `igvk64.dll` (Intel).
|
||||
- It happens with the installer and the portable version.
|
||||
|
||||
## Fix
|
||||
|
||||
1. Install the [latest release](https://github.com/storytold/designcraft/releases/latest). DesignCraft remembers a start that never showed a window and tries the next graphics backend on the following start. On Windows the order is DirectX 12, Vulkan, then OpenGL, and the status bar says when it switches. A reporter who could not start 0.5.0 on Windows 11 Enterprise wrote that 0.6.0 launched without trouble ([issue #273](https://github.com/storytold/designcraft/issues/273)).
|
||||
2. If it still closes, set an environment variable named `WGPU_BACKEND` with the value `dx12`. Start DesignCraft from a shell where it is set, or add it in Windows settings for your account. People who hit this crash with an AMD graphics driver reported that DesignCraft starts with `WGPU_BACKEND=dx12`. `dx12` also avoids Intel's Vulkan driver (`igvk64.dll`), which crashed in [issue #192](https://github.com/storytold/designcraft/issues/192).
|
||||
3. Start DesignCraft again.
|
||||
|
||||
Setting `WGPU_BACKEND` also turns off the automatic fallback. Other accepted values are `vulkan` and `gl`, and a comma-separated list lets the graphics library choose among them. Do not pick `vulkan` on Intel graphics or `gl` on AMD graphics: those are the drivers in the crash reports.
|
||||
|
||||
## Why this happens
|
||||
|
||||
DesignCraft draws the canvas on the CPU and uses the graphics card only to show the interface. Some graphics drivers crash while the window is being set up, and a crash in a driver ends the program before DesignCraft can react. One AMD laptop's Vulkan driver crashed on the first frame, which is why DirectX 12 is the default on Windows.
|
||||
|
||||
## Start the backend search over
|
||||
|
||||
DesignCraft keeps its record of failed backends in a file called `gpu.json` in its settings folder, the one that holds `ui.json`, `prefs.json` and the `logs` folder (`%APPDATA%\DesignCraft\logs\` is the log folder on Windows). Delete the file to start the search again. The list also starts over after every backend has failed, and with a new DesignCraft version.
|
||||
|
||||
## Still stuck?
|
||||
|
||||
Send the log. See [Report a problem](get-help.md) for where it is. Or ask in [Discord](https://discord.gg/artcraft).
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Report a problem
|
||||
description: What to include and where to post when DesignCraft misbehaves or crashes.
|
||||
order: 20
|
||||
verified: 0.7.1
|
||||
---
|
||||
|
||||
Post in the [issue tracker](https://github.com/storytold/designcraft/issues) or in [Discord](https://discord.gg/artcraft), and attach the log file. The log is the most useful thing you can send after a crash.
|
||||
|
||||
## What to include
|
||||
|
||||
- Your DesignCraft version and how you installed it (installer, portable, AppImage and so on).
|
||||
- Your operating system and graphics card.
|
||||
- What you did, what you expected and what happened.
|
||||
- The log file `designcraft.log`.
|
||||
|
||||
## Where the log is
|
||||
|
||||
DesignCraft writes `logs/designcraft.log` in its settings folder, beside `ui.json` and `prefs.json`.
|
||||
|
||||
| System | Log folder |
|
||||
|---------|-------------------------------------------------------------------------|
|
||||
| Windows | `%APPDATA%\DesignCraft\logs\` |
|
||||
| macOS | `~/Library/Application Support/DesignCraft/logs/` |
|
||||
| Linux | `~/.config/designcraft/logs/` (or `$XDG_CONFIG_HOME/designcraft/logs/`) |
|
||||
|
||||
Each start moves the previous log to `designcraft.1.log`, and that one to `designcraft.2.log`. The log from a run that crashed survives the next start, so attach `designcraft.1.log` if you have restarted since. The file stops growing at 16 MiB.
|
||||
|
||||
## More detail in the log
|
||||
|
||||
Set the environment variable `RUST_LOG` to `debug` before starting DesignCraft to log more. The default is `info` for DesignCraft's own parts and `warn` for everything else.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Troubleshooting
|
||||
description: Fixes for common DesignCraft problems.
|
||||
order: 30
|
||||
---
|
||||
|
||||
- [DesignCraft closes right after it starts](app-closes-at-start.md)
|
||||
- [Report a problem](get-help.md)
|
||||
@@ -135,15 +135,40 @@ ln -s /Applications "$STAGE/Applications"
|
||||
mkdir -p "$STAGE/.background"
|
||||
cp "$HERE/dmg/background.tiff" "$STAGE/.background/background.tiff"
|
||||
cp "$HERE/dmg/dmg-layout.DS_Store" "$STAGE/.DS_Store"
|
||||
rm -f "$DMG" "$WORK/raw.dmg"
|
||||
# makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`,
|
||||
# which is flaky on CI runners ("Resource busy") and hangs in sandboxed sessions.
|
||||
# The volume name has no version: .DS_Store finds the background through an alias that includes it.
|
||||
hdiutil makehybrid -hfs -hfs-volume-name "DesignCraft" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE"
|
||||
hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG"
|
||||
rm -f "$WORK/raw.dmg"
|
||||
rm -f "$DMG"
|
||||
# `hdiutil create -srcfolder` writes the files as they are. `makehybrid -hfs` gave every file a
|
||||
# com.apple.FinderInfo, which `codesign --verify --strict` rejects as "Finder information, or
|
||||
# similar detritus" on the app in the image and on a copy installed from it (as VectorCraft's
|
||||
# storytold/vectorcraft#1085 and PdfCraft's storytold/pdfcraft#823). `create` attaches a device
|
||||
# while it copies, which fails now and then on CI runners ("Resource busy"), so it gets a few
|
||||
# tries. The volume name has no version: .DS_Store finds the background through an alias that
|
||||
# includes it.
|
||||
# Usage: hdiutil_retry <what> <hdiutil args…>; tries five times with growing pauses.
|
||||
hdiutil_retry() {
|
||||
local what="$1" attempt
|
||||
shift
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if hdiutil "$@"; then
|
||||
return 0
|
||||
fi
|
||||
[ "$attempt" = 5 ] && { echo "hdiutil $what failed 5 times" >&2; return 1; }
|
||||
echo "hdiutil $what failed (attempt $attempt of 5); retrying in $((attempt * 10))s" >&2
|
||||
sleep $((attempt * 10))
|
||||
done
|
||||
}
|
||||
hdiutil_retry create create -srcfolder "$STAGE" -volname "DesignCraft" -fs HFS+ -format UDZO -imagekey zlib-level=9 -ov "$DMG"
|
||||
sign "$DMG"
|
||||
codesign --verify --strict --verbose=2 "$DMG"
|
||||
# The app inside the image must still pass --strict, so a makehybrid-style regression can't ship.
|
||||
MOUNT="$WORK/dmg-check"
|
||||
rm -rf "$MOUNT"
|
||||
mkdir -p "$MOUNT"
|
||||
hdiutil_retry attach attach -nobrowse -readonly -mountpoint "$MOUNT" "$DMG" >/dev/null
|
||||
if ! codesign --verify --strict --deep --verbose=2 "$MOUNT/DesignCraft.app"; then
|
||||
hdiutil_retry detach detach "$MOUNT" >/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
hdiutil_retry detach detach "$MOUNT" >/dev/null
|
||||
if [ "$NOTARIZE" = 1 ]; then
|
||||
notarize "$DMG"
|
||||
xcrun stapler staple "$DMG"
|
||||
|
||||
Reference in New Issue
Block a user