Spaces:
Running on Zero
Running on Zero
deploy: update Space from deploy_preflight --push
Browse files- app.py +5 -6
- core/theme.py +10 -2
- core/widgets.py +34 -12
app.py
CHANGED
|
@@ -913,13 +913,12 @@ def build() -> gr.Blocks:
|
|
| 913 |
gr.HTML(tab_intro("The pre-flight check, <b>before it prints</b>: slice the part, read "
|
| 914 |
"precedent, flag failures, and get a second opinion. Then → PRINT."))
|
| 915 |
with gr.Group(visible=False, elem_classes=["ce-part-card"]) as build_results:
|
| 916 |
-
# slice + motion preview side by side —
|
| 917 |
-
gr.HTML(rule("SLICE · CROSS-SECTION"))
|
| 918 |
-
with gr.Row(equal_height=False, elem_classes=["ce-part-row"]):
|
| 919 |
-
with gr.Column(scale=
|
| 920 |
vp_layer = gr.Image(label="", height=360, show_label=False)
|
| 921 |
-
with gr.Column(scale=1, elem_classes=["ce-vp"]):
|
| 922 |
-
gr.HTML(rule("MOTION PREVIEW"))
|
| 923 |
vprint = gr.HTML()
|
| 924 |
with gr.Column(elem_classes=["ce-hslider"]):
|
| 925 |
vp_slider = gr.Slider(1, SCRUB_LAYERS, value=1, step=1,
|
|
|
|
| 913 |
gr.HTML(tab_intro("The pre-flight check, <b>before it prints</b>: slice the part, read "
|
| 914 |
"precedent, flag failures, and get a second opinion. Then → PRINT."))
|
| 915 |
with gr.Group(visible=False, elem_classes=["ce-part-card"]) as build_results:
|
| 916 |
+
# slice + motion preview side by side — equal width columns
|
| 917 |
+
gr.HTML(rule("SLICE · CROSS-SECTION + MOTION PREVIEW"))
|
| 918 |
+
with gr.Row(equal_height=False, elem_classes=["ce-part-row", "ce-slice-viz"]):
|
| 919 |
+
with gr.Column(scale=1, elem_classes=["ce-slice-col"]):
|
| 920 |
vp_layer = gr.Image(label="", height=360, show_label=False)
|
| 921 |
+
with gr.Column(scale=1, elem_classes=["ce-slice-col", "ce-vp"]):
|
|
|
|
| 922 |
vprint = gr.HTML()
|
| 923 |
with gr.Column(elem_classes=["ce-hslider"]):
|
| 924 |
vp_slider = gr.Slider(1, SCRUB_LAYERS, value=1, step=1,
|
core/theme.py
CHANGED
|
@@ -563,11 +563,19 @@ CSS += """
|
|
| 563 |
|
| 564 |
/* Virtual print preview — bigger, framed, motion-aware */
|
| 565 |
.ce-vp { padding:8px !important; border:1px solid var(--ao-outline-dim) !important;
|
| 566 |
-
background:var(--ao-void) !important;
|
| 567 |
-
|
|
|
|
| 568 |
.ce-vp svg { width:100% !important; height:auto !important; max-height:300px !important; }
|
| 569 |
.ce-vp .ce-vp-caption { color:var(--ao-outline); font-size:11px;
|
| 570 |
letter-spacing:1px; text-transform:uppercase; padding:4px 2px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
|
| 572 |
/* Mini loader (Second Opinion in-flight — smaller than the full preflight loader).
|
| 573 |
Same scanning-bar look but condensed. */
|
|
|
|
| 563 |
|
| 564 |
/* Virtual print preview — bigger, framed, motion-aware */
|
| 565 |
.ce-vp { padding:8px !important; border:1px solid var(--ao-outline-dim) !important;
|
| 566 |
+
background:var(--ao-void) !important; display:flex !important; flex-direction:column !important; gap:6px !important; }
|
| 567 |
+
.ce-vp-wrap { border:1px solid var(--ao-outline-dim); background:#0a0c14; display:inline-block; }
|
| 568 |
+
.ce-vp canvas { display:block; max-width:100%; height:auto; }
|
| 569 |
.ce-vp svg { width:100% !important; height:auto !important; max-height:300px !important; }
|
| 570 |
.ce-vp .ce-vp-caption { color:var(--ao-outline); font-size:11px;
|
| 571 |
letter-spacing:1px; text-transform:uppercase; padding:4px 2px; }
|
| 572 |
+
.ce-vp-replay { align-self:flex-start; }
|
| 573 |
+
/* side-by-side slice visualizers: equal-width columns */
|
| 574 |
+
.ce-slice-viz { gap:16px !important; }
|
| 575 |
+
.ce-slice-viz .ce-slice-col { flex:1 1 0 !important; min-width:0 !important; }
|
| 576 |
+
.ce-slice-viz .ce-slice-col .gr-image,
|
| 577 |
+
.ce-slice-viz .ce-slice-col .gr-image > div,
|
| 578 |
+
.ce-slice-viz .ce-slice-col .gr-image img { width:100% !important; height:auto !important; display:block !important; }
|
| 579 |
|
| 580 |
/* Mini loader (Second Opinion in-flight — smaller than the full preflight loader).
|
| 581 |
Same scanning-bar look but condensed. */
|
core/widgets.py
CHANGED
|
@@ -26,9 +26,7 @@ def _projected_layers(mesh_path: str | Path, layer_height_mm: float = 0.2,
|
|
| 26 |
|
| 27 |
{bounds:[xmin,xmax,ymin,ymax], layers:[[[x0,y0,x1,y1],...], ...]} — projected
|
| 28 |
oblique screen space, rounded to keep the data-attribute payload small. Segments
|
| 29 |
-
per layer are capped (down-sampled) so a high-res mesh doesn't bloat the page
|
| 30 |
-
the rising animation only needs enough segments to read; the layer SCRUBBER
|
| 31 |
-
(layer_image) renders full fidelity server-side.
|
| 32 |
"""
|
| 33 |
from sim.virtual_printer import slice_segments
|
| 34 |
|
|
@@ -140,7 +138,8 @@ def virtual_printer_html(mesh_path: str | Path | None,
|
|
| 140 |
"""A live virtual-printer canvas for the cockpit (animated by VP_JS).
|
| 141 |
|
| 142 |
Uses the layer height from the proposed PrintSettings so the preview is tied
|
| 143 |
-
to the actual recommendation rather than a hard-coded default.
|
|
|
|
| 144 |
"""
|
| 145 |
if not mesh_path or not Path(str(mesh_path)).exists():
|
| 146 |
return ("<div class='ce-rule'>VIRTUAL PRINT</div>"
|
|
@@ -152,13 +151,28 @@ def virtual_printer_html(mesh_path: str | Path | None,
|
|
| 152 |
"<div class='ce-sub'>mesh too thin to slice.</div>")
|
| 153 |
payload = json.dumps(data).replace("'", "'")
|
| 154 |
lh_note = f"{layer_height:.2f} mm layers"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
return (
|
| 156 |
-
"<div class='ce-rule'>VIRTUAL PRINT</div>"
|
| 157 |
-
"<div
|
| 158 |
-
f"<canvas class='ce-vp' width='
|
| 159 |
-
"style='width:
|
| 160 |
-
|
| 161 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
)
|
| 163 |
|
| 164 |
|
|
@@ -178,6 +192,12 @@ VP_HEAD = r"""
|
|
| 178 |
|
| 179 |
// --- virtual-printer animator ---
|
| 180 |
const DONE='#46627f', CUR='#ff9c00', NOZ='#ffe6b4', BG='#0a0c14';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
function animate(cv){
|
| 182 |
let data; try { data = JSON.parse(cv.getAttribute('data-vp')); } catch(e){ return; }
|
| 183 |
if(!data || !data.layers || !data.layers.length) return;
|
|
@@ -188,6 +208,8 @@ VP_HEAD = r"""
|
|
| 188 |
const X = x => pad + (x-xmin)*s, Y = y => H - (pad + (y-ymin)*s);
|
| 189 |
const N = data.layers.length;
|
| 190 |
let upto = 0, hold = 0;
|
|
|
|
|
|
|
| 191 |
function frame(){
|
| 192 |
ctx.fillStyle=BG; ctx.fillRect(0,0,W,H);
|
| 193 |
for(let li=0; li<=upto && li<N; li++){
|
|
@@ -204,8 +226,8 @@ VP_HEAD = r"""
|
|
| 204 |
// HUD
|
| 205 |
ctx.fillStyle='#9fb0c8'; ctx.font='10px ui-monospace,monospace';
|
| 206 |
ctx.fillText('LAYER '+(upto+1)+'/'+N+' · '+Math.round(100*(upto+1)/N)+'%', pad, 14);
|
| 207 |
-
if(upto < N-1){ if(++hold>=
|
| 208 |
-
// else:
|
| 209 |
}
|
| 210 |
frame();
|
| 211 |
}
|
|
|
|
| 26 |
|
| 27 |
{bounds:[xmin,xmax,ymin,ymax], layers:[[[x0,y0,x1,y1],...], ...]} — projected
|
| 28 |
oblique screen space, rounded to keep the data-attribute payload small. Segments
|
| 29 |
+
per layer are capped (down-sampled) so a high-res mesh doesn't bloat the page.
|
|
|
|
|
|
|
| 30 |
"""
|
| 31 |
from sim.virtual_printer import slice_segments
|
| 32 |
|
|
|
|
| 138 |
"""A live virtual-printer canvas for the cockpit (animated by VP_JS).
|
| 139 |
|
| 140 |
Uses the layer height from the proposed PrintSettings so the preview is tied
|
| 141 |
+
to the actual recommendation rather than a hard-coded default. Canvas size is
|
| 142 |
+
chosen from the projected mesh aspect ratio so the preview is not squished.
|
| 143 |
"""
|
| 144 |
if not mesh_path or not Path(str(mesh_path)).exists():
|
| 145 |
return ("<div class='ce-rule'>VIRTUAL PRINT</div>"
|
|
|
|
| 151 |
"<div class='ce-sub'>mesh too thin to slice.</div>")
|
| 152 |
payload = json.dumps(data).replace("'", "'")
|
| 153 |
lh_note = f"{layer_height:.2f} mm layers"
|
| 154 |
+
|
| 155 |
+
# Canvas size derived from projected bounds so the preview is not squished.
|
| 156 |
+
# Max width is clamped to the column; height follows aspect ratio.
|
| 157 |
+
xmin, xmax, ymin, ymax = data["bounds"]
|
| 158 |
+
proj_w = max(xmax - xmin, 1)
|
| 159 |
+
proj_h = max(ymax - ymin, 1)
|
| 160 |
+
aspect = proj_h / proj_w
|
| 161 |
+
max_css_w = 560
|
| 162 |
+
css_h = min(round(max_css_w * aspect), 340)
|
| 163 |
+
canvas_w, canvas_h = max_css_w, css_h
|
| 164 |
+
|
| 165 |
return (
|
| 166 |
+
"<div class='ce-rule'>VIRTUAL PRINT · MOTION PREVIEW</div>"
|
| 167 |
+
"<div class='ce-vp-wrap'>"
|
| 168 |
+
f"<canvas class='ce-vp' width='{canvas_w}' height='{canvas_h}' data-vp='{payload}' "
|
| 169 |
+
f"data-aspect='{aspect:.4f}' style='width:{max_css_w}px;height:{css_h}px;display:block;'></canvas>"
|
| 170 |
+
"</div>"
|
| 171 |
+
f"<div class='ce-sub ce-vp-caption'>{caption} · {lh_note} · real cross-sections of "
|
| 172 |
+
"this part, rising layer by layer (motion preview — not a slicer). Click REPLAY to restart.</div>"
|
| 173 |
+
"<button class='ce-pillbtn ce-vp-replay' type='button' "
|
| 174 |
+
"onclick=\"const cv=this.parentNode.querySelector('canvas.ce-vp');"
|
| 175 |
+
"if(window.__vp_replay) window.__vp_replay(cv);\">REPLAY</button>"
|
| 176 |
)
|
| 177 |
|
| 178 |
|
|
|
|
| 192 |
|
| 193 |
// --- virtual-printer animator ---
|
| 194 |
const DONE='#46627f', CUR='#ff9c00', NOZ='#ffe6b4', BG='#0a0c14';
|
| 195 |
+
window.__vp_replay = function(cv){
|
| 196 |
+
if(cv._raf){ cancelAnimationFrame(cv._raf); cv._raf=null; }
|
| 197 |
+
cv.removeAttribute('data-init');
|
| 198 |
+
const ctx = cv.getContext('2d'); ctx.fillStyle=BG; ctx.fillRect(0,0,cv.width,cv.height);
|
| 199 |
+
scan();
|
| 200 |
+
};
|
| 201 |
function animate(cv){
|
| 202 |
let data; try { data = JSON.parse(cv.getAttribute('data-vp')); } catch(e){ return; }
|
| 203 |
if(!data || !data.layers || !data.layers.length) return;
|
|
|
|
| 208 |
const X = x => pad + (x-xmin)*s, Y = y => H - (pad + (y-ymin)*s);
|
| 209 |
const N = data.layers.length;
|
| 210 |
let upto = 0, hold = 0;
|
| 211 |
+
// bigger hold count = slower animation. default 6; override with window.__vp_speed.
|
| 212 |
+
const speed = Math.max(1, Math.min(20, Number(window.__vp_speed || 6)));
|
| 213 |
function frame(){
|
| 214 |
ctx.fillStyle=BG; ctx.fillRect(0,0,W,H);
|
| 215 |
for(let li=0; li<=upto && li<N; li++){
|
|
|
|
| 226 |
// HUD
|
| 227 |
ctx.fillStyle='#9fb0c8'; ctx.font='10px ui-monospace,monospace';
|
| 228 |
ctx.fillText('LAYER '+(upto+1)+'/'+N+' · '+Math.round(100*(upto+1)/N)+'%', pad, 14);
|
| 229 |
+
if(upto < N-1){ if(++hold>=speed){ hold=0; upto++; } cv._raf = requestAnimationFrame(frame); }
|
| 230 |
+
// else: full part drawn — hold final frame
|
| 231 |
}
|
| 232 |
frame();
|
| 233 |
}
|