From d3fbca9a7c9a72bc36741d81caaa7a67cdb9cc2b Mon Sep 17 00:00:00 2001 From: stefanodangelo <61756862+stefanodangelo@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:12:49 +0100 Subject: [PATCH] feat: estimate card and swap timeline in the preflight dialog --- multiace/i18n/de.json | 22 +++++++++- multiace/i18n/en.json | 22 +++++++++- multiace/i18n/zh.json | 22 +++++++++- multiace/web/frontend/app.js | 74 +++++++++++++++++++++++++++++++- multiace/web/frontend/index.html | 64 +++++++++++++++++++++++++++ multiace/web/frontend/style.css | 27 ++++++++++++ 6 files changed, 224 insertions(+), 7 deletions(-) diff --git a/multiace/i18n/de.json b/multiace/i18n/de.json index de57837..25898d5 100644 --- a/multiace/i18n/de.json +++ b/multiace/i18n/de.json @@ -605,7 +605,23 @@ "tier_name_base": "Name·Grundton", "tier_name_canon": "Name·Synonym", "tier_no_slot": "kein Slot ⚠", - "title": "G-Code Preflight" + "title": "G-Code Preflight", + "est_title": "Schätzung", + "est_badge": "geschätzt", + "est_conf_degraded": "unvollständige Daten", + "est_conf_calibrated": "kalibriert", + "est_updating": "wird aktualisiert...", + "est_slicer": "Slicer-Zeit", + "est_total": "Mit multiACE", + "est_added": "Durch Wechsel hinzugefügt", + "est_filament": "Filament", + "est_purge": "Spülung", + "est_no_window": "ohne bekanntes Zeitfenster", + "est_timeline": "Wechsel-Zeitleiste", + "est_colour": "Farbe", + "est_print": "Druck", + "est_sum": "Gesamt", + "est_assumptions": "Annahmen" }, "queue": { "clear_errors": "Fehler löschen", @@ -1040,7 +1056,9 @@ "swap_kind": { "first_load": "Erstes Laden", "same_ace": "Gleiche MMU", - "cross_ace_inline": "MMU-übergreifend" + "cross_ace_inline": "MMU-übergreifend", + "cross_ace_bg": "Hintergrund", + "head_change": "Kopfwechsel" } } }, diff --git a/multiace/i18n/en.json b/multiace/i18n/en.json index 03957bb..40b7530 100644 --- a/multiace/i18n/en.json +++ b/multiace/i18n/en.json @@ -604,7 +604,23 @@ "tier_name_base": "name·base", "tier_name_canon": "name·synonym", "tier_no_slot": "no slot ⚠", - "title": "G-code preflight" + "title": "G-code preflight", + "est_title": "Estimate", + "est_badge": "estimated", + "est_conf_degraded": "partial data", + "est_conf_calibrated": "calibrated", + "est_updating": "updating...", + "est_slicer": "Slicer time", + "est_total": "With multiACE", + "est_added": "Added by swaps", + "est_filament": "Filament", + "est_purge": "Purge", + "est_no_window": "without a known window", + "est_timeline": "Swap timeline", + "est_colour": "Colour", + "est_print": "Print", + "est_sum": "Total", + "est_assumptions": "Assumptions" }, "queue": { "clear_errors": "Clear errors", @@ -1039,7 +1055,9 @@ "swap_kind": { "first_load": "First load", "same_ace": "Same MMU", - "cross_ace_inline": "Cross-MMU" + "cross_ace_inline": "Cross-MMU", + "cross_ace_bg": "Background", + "head_change": "Head change" } } }, diff --git a/multiace/i18n/zh.json b/multiace/i18n/zh.json index d21c9a0..c932a5f 100644 --- a/multiace/i18n/zh.json +++ b/multiace/i18n/zh.json @@ -284,7 +284,23 @@ "not_feasible": "不可行", "slicer": "切片", "printer": "打印机", - "match_tier": "匹配" + "match_tier": "匹配", + "est_title": "预估", + "est_badge": "估算值", + "est_conf_degraded": "数据不完整", + "est_conf_calibrated": "已校准", + "est_updating": "更新中...", + "est_slicer": "切片软件时间", + "est_total": "使用 multiACE", + "est_added": "换料增加", + "est_filament": "耗材", + "est_purge": "冲洗", + "est_no_window": "窗口未知", + "est_timeline": "换料时间线", + "est_colour": "颜色", + "est_print": "打印", + "est_sum": "合计", + "est_assumptions": "假设" }, "queue": { "send_all_tooltip": "将所有排队命令发送到打印机(之后可以关闭界面)", @@ -335,7 +351,9 @@ "swap_kind": { "first_load": "首次装载", "same_ace": "同一 MMU", - "cross_ace_inline": "跨 MMU" + "cross_ace_inline": "跨 MMU", + "cross_ace_bg": "后台", + "head_change": "喷头切换" } } }, diff --git a/multiace/web/frontend/app.js b/multiace/web/frontend/app.js index 33b6731..3b1b892 100644 --- a/multiace/web/frontend/app.js +++ b/multiace/web/frontend/app.js @@ -6895,6 +6895,7 @@ createApp({ preflight.slicerSwaps = realSwapCount( (preflight.report && preflight.report.events) || [], _slicerEffectiveMapping()); + scheduleEstimate("slicer"); } function slicerSwapsDisplay() { if (preflight.slicerSwaps !== null) return preflight.slicerSwaps; @@ -7341,6 +7342,7 @@ createApp({ preflight.headSwaps = headSwapCount( (preflight.report && preflight.report.events) || [], _headEffectiveAssignment(), headCopiesEffective()); + scheduleEstimate("loadout"); } function headSwapsDisplay() { if (preflight.headSwaps !== null) return preflight.headSwaps; @@ -7811,6 +7813,76 @@ createApp({ for (const p of list) if (pfPlanSwaps(p) < pfPlanSwaps(best)) best = p; return best === list[0] ? null : best; } + // ---- estimate card ---------------------------------------------------- + // Every number is a model estimate (never measured). A plan's estimate + // comes with the report; after a mapping edit the browser worker + // recomputes it for the plan being edited (pfEstLive), without a re-parse. + const pfEstLive = reactive({}); + let pfEstTimer = null; + let pfEstSeq = 0; + function pfEst(p) { + const live = pfEstLive[p]; + if (live && live.estimate) return live.estimate; + const pl = preflight.report && preflight.report.plans && preflight.report.plans[p]; + return (pl && pl.estimate) || null; + } + function pfTimeline(p) { + const live = pfEstLive[p]; + if (live && live.timeline) return live.timeline; + const pl = preflight.report && preflight.report.plans && preflight.report.plans[p]; + return (pl && pl.timeline) || []; + } + function pfEstDelta(e) { + return (e && e.added_s !== null && e.added_s !== undefined) ? "+" + fmtDuration(e.added_s) : ""; + } + // Per-kind share of the added time, for the timeline strip. + function pfTimelineSegs(p) { + const evs = pfTimeline(p).filter(ev => ev.kind !== "first_load" && Number(ev.seconds) > 0); + const total = evs.reduce((a, ev) => a + (Number(ev.seconds) || 0), 0) || 1; + return evs.map((ev, i) => ({ + key: i, kind: ev.kind, t: ev.t, + pct: Math.max(0.4, 100 * (Number(ev.seconds) || 0) / total), + tip: "T" + ev.t + " - " + swapKindLabel(ev.kind) + + " - " + fmtDuration(ev.seconds) + + (ev.purge_mm ? " - " + Math.round(ev.purge_mm) + " mm" : ""), + })); + } + function pfEstCounts(e) { + const parts = []; + if (e.inline_swaps) parts.push(swapKindLabel("cross_ace_inline") + " x " + e.inline_swaps); + if (e.bg_swaps) parts.push(swapKindLabel("cross_ace_bg") + " x " + e.bg_swaps); + if (e.head_changes) parts.push(swapKindLabel("head_change") + " x " + e.head_changes); + return parts.join(" · "); + } + // Live recompute after a mapping/target edit. Only the in-browser path + // can do it (the server has no endpoint for it); otherwise the numbers + // keep showing the analysed assignment, flagged stale. + const pfEstStale = ref(false); + function scheduleEstimate(plan) { + if (!preflight.local || !preflightJobId) return; + pfEstStale.value = true; + clearTimeout(pfEstTimer); + pfEstTimer = setTimeout(async () => { + const seq = ++pfEstSeq; + const payload = {jobId: preflightJobId, plan}; + if (plan === "slicer") { + payload.mapping = _slicerEffectiveMapping(); + } else { + payload.targetIds = _headEffectiveAssignment(); + payload.copyIds = headCopiesEffective(); + } + try { + const j = await runPreflightWorker("estimate", payload); + if (seq !== pfEstSeq) return; + if (j.result) pfEstLive[plan] = j.result; + pfEstStale.value = false; + } catch (_) { /* keep the analysed numbers */ } + }, 200); + } + watch(() => preflight.report, () => { + for (const k of Object.keys(pfEstLive)) delete pfEstLive[k]; + pfEstStale.value = false; + }); function pfPlanTitle(p) { return t(preflight.report && preflight.report.head_mode ? "ui.preflight.head_plan_" + p : "ui.preflight.plan_" + p); @@ -8837,7 +8909,7 @@ createApp({ headTargets, headTargetOptions, headEffectiveTargetId, headTargetLabel, headTargetColor, chipBg, headTargetLabelById, onHeadTargetChange, headSwapsDisplay, hmDropOpen, hmDdToggle, hmDdClose, hmDdPick, - pfPlanSel, pfPlanList, pfPlanFeasible, pfPlanSwaps, pfPlanFlush, pfPlanBg, pfSelected, pfRecommended, pfPlanTitle, pfGoLabel, pfPrintSelected, + pfPlanSel, pfEst, pfTimelineSegs, pfEstCounts, pfEstDelta, pfEstStale, pfPlanList, pfPlanFeasible, pfPlanSwaps, pfPlanFlush, pfPlanBg, pfSelected, pfRecommended, pfPlanTitle, pfGoLabel, pfPrintSelected, headFeasible, headPlanFeasible, headPlanSwaps, headPlanBg, headPlanFlushG, headPlanBgLabel, headSlicerHex, headSlicerMat, headProposalLabel, headMaxCopies, headCopiesEffective, headCopyPlan, headCopyList, copySlotLabel, setPreflightCopies, diff --git a/multiace/web/frontend/index.html b/multiace/web/frontend/index.html index f855723..7163644 100644 --- a/multiace/web/frontend/index.html +++ b/multiace/web/frontend/index.html @@ -3679,6 +3679,37 @@
{{ headPlanBgLabel(pfSelected()) }}
+{{ pfEstCounts(pfEst(pfSelected())) }} · {{ pfEst(pfSelected()).unknown_window_swaps }} {{ t('ui.preflight.est_no_window') }}
+| {{ t('ui.preflight.est_colour') }} | {{ t('ui.preflight.est_print') }} | {{ t('ui.preflight.est_purge') }} | {{ t('ui.preflight.est_sum') }} |
|---|---|---|---|
| T{{ dispIdx(c.t) }} | {{ c.print_g }} g | {{ c.purge_g }} g | {{ (c.print_g + c.purge_g).toFixed(1) }} g | +
{{ headPlanBgLabel(pfSelected()) }}
+{{ pfEstCounts(pfEst(pfSelected())) }} · {{ pfEst(pfSelected()).unknown_window_swaps }} {{ t('ui.preflight.est_no_window') }}
+| {{ t('ui.preflight.est_colour') }} | {{ t('ui.preflight.est_print') }} | {{ t('ui.preflight.est_purge') }} | {{ t('ui.preflight.est_sum') }} |
|---|---|---|---|
| T{{ dispIdx(c.t) }} | {{ c.print_g }} g | {{ c.purge_g }} g | {{ (c.print_g + c.purge_g).toFixed(1) }} g | +