first commit
Verify / test (push) Failing after 2m54s

This commit is contained in:
2026-11-05 17:31:17 +03:30
کامیت 56c5ee23d3
63فایلهای تغییر یافته به همراه2749 افزوده شده و 0 حذف شده
+20
مشاهده پرونده
@@ -0,0 +1,20 @@
/** SVG viewBox camera. All pointer math is in viewport pixels / document units. */
export class Camera {
constructor(bounds, width, height) { this.bounds = bounds; this.resize(width, height); this.fit(); }
resize(width, height) { this.width = Math.max(1, width); this.height = Math.max(1, height); }
fit() {
const b = this.bounds;
this.scale = Math.min(this.width / b.width, this.height / b.height);
this.fitScale = this.scale;
this.x = (this.width - b.width * this.scale) / 2 - b.x * this.scale;
this.y = (this.height - b.height * this.scale) / 2 - b.y * this.scale;
}
world(x, y) { return { x: (x - this.x) / this.scale, y: (y - this.y) / this.scale }; }
zoom(factor, x, y) {
const p = this.world(x, y);
this.scale = Math.min(this.fitScale * 200, Math.max(this.fitScale * .05, this.scale * factor));
this.x = x - p.x * this.scale; this.y = y - p.y * this.scale;
}
pan(x, y) { this.x += x; this.y += y; }
get viewBox() { return `${-this.x / this.scale} ${-this.y / this.scale} ${this.width / this.scale} ${this.height / this.scale}`; }
}
+19
مشاهده پرونده
@@ -0,0 +1,19 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Camera } from './camera.js';
const close = (a,b) => assert.ok(Math.abs(a-b)<1e-8,`${a} != ${b}`);
test('fit preserves sheet aspect ratio without padding',()=> {
const c = new Camera({x:0,y:-760,width:1150,height:760},1920,1080);
close(c.scale,1080/760); close(c.y,1080);
const center = c.world(960,540); close(center.x,575); close(center.y,-380);
});
test('zoom retains exact world coordinate under cursor',()=> {
const c = new Camera({x:0,y:-760,width:1150,height:760},1000,800);
const p = c.world(121,534); c.zoom(3.2,121,534); const q = c.world(121,534);
close(p.x,q.x); close(p.y,q.y); c.pan(12,-8); const r=c.world(133,526); close(r.x,p.x); close(r.y,p.y);
});
test('zoom is bounded for hostile wheel deltas',()=> {
const c = new Camera({x:0,y:0,width:100,height:100},1000,800);
c.zoom(1e30,0,0); close(c.scale,c.fitScale*200);
c.zoom(1e-30,0,0); close(c.scale,c.fitScale*.05);
});
+1
مشاهده پرونده
@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Schematic API reference</title><link rel="stylesheet" href="/assets/upload.css"></head><body><main><p class="eyebrow">API V1</p><h1>Schematic rendering API</h1><p>Authenticate uploads with <code>X-API-Key</code>. Native binary OLE and ASCII SchDoc format 5.0 are accepted. Files are parsed asynchronously; HTTP 202 confirms admission, not rendering fidelity.</p><h2>Upload</h2><p><code>POST /api/v1/schematics/render</code></p><p>Multipart fields: <code>file</code> (required), <code>access</code> (private or unlisted; defaults to private), <code>expires_in</code> (optional seconds, bounded by server policy).</p><h2>Poll</h2><p><code>GET /api/v1/schematics/{id}/status</code></p><p>States: queued, processing, completed, failed. Private reads require <code>X-Viewer-Token</code> from the upload response, or the service API key. Completed responses include compatibility and diagnostics.</p><h2>Retrieve</h2><p><code>GET /api/v1/schematics/{id}/data</code> returns the versioned structured document. <code>GET /api/v1/schematics/{id}/svg</code> returns the generated vector drawing.</p><h2>View</h2><p>Open the exact <code>viewer_url</code> from the upload response. The private token is in its fragment. It is stored for this browser tab and sent only as a request header. The viewer has no panels or toolbar.</p><h2>Errors</h2><p>JSON errors include <code>success: false</code> and <code>error.code</code> / <code>error.message</code>. Common HTTP codes: 401 invalid service key; 404 missing or inaccessible schematic; 409 still processing; 410 expired; 413 too large; 415 wrong extension; 422 invalid schematic; 503 storage unavailable or queue full.</p><p><a href="/openapi.json">Download OpenAPI schema</a> · <a href="/">Upload a schematic</a></p></main></body></html>
+2
مشاهده پرونده
@@ -0,0 +1,2 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Altium schematic upload</title><link rel="stylesheet" href="/assets/upload.css"><script type="module" src="/assets/upload.js"></script></head><body><main><p class="eyebrow">NATIVE ALTIUM · SCHDOC</p><h1>Open your schematic.</h1><p>Upload a native schematic to create a private, expiring viewer link.</p><form id="upload"><label for="key">API key</label><input id="key" type="password" autocomplete="off" placeholder="Configured service key"><label for="file">Schematic file</label><input id="file" type="file" accept=".SchDoc" required><button type="submit">Render schematic <span aria-hidden="true">↗</span></button></form><output id="result" role="status"></output><p class="note">Compatibility diagnostics are returned by the API. Review them before relying on visual fidelity.</p><a href="/docs">API reference</a></main></body></html>
+15
مشاهده پرونده
@@ -0,0 +1,15 @@
:root { font-family: system-ui,sans-serif; color: #182f31; background: #f5f6f3; color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; min-height: 100dvh; display: grid; place-items: center; padding: 28px; }
main { width: min(100%,500px); }
.eyebrow { color: #51706d; letter-spacing: .16em; font-size: 11px; font-weight: 700; }
h1 { font-size: clamp(30px,6vw,42px); letter-spacing: -.04em; margin: 16px 0; }
p { font-size: 15px; line-height: 1.6; color: #526462; }
form { margin: 28px 0; display: grid; gap: 10px; }
label { font-size: 13px; margin-top: 10px; }
input { padding: 13px; border: 1px solid #ccd4ce; background: white; font: inherit; border-radius: 5px; width: 100%; }
button { display: flex; justify-content: space-between; margin-top: 12px; padding: 16px; background: #244d49; color: white; border: none; border-radius: 5px; font: inherit; cursor: pointer; }
button:disabled { opacity: .5; cursor: wait; }
output { display: block; font-size: 14px; overflow-wrap: anywhere; line-height: 1.6; }
a { color: #244d49; font-size: 13px; }
.note { margin-top: 28px; font-size: 12px; }
+16
مشاهده پرونده
@@ -0,0 +1,16 @@
const form = document.querySelector('#upload'), result = document.querySelector('#result');
form.addEventListener('submit',async e => {
e.preventDefault();
const button = form.querySelector('button'); button.disabled = true; result.textContent = 'Uploading…';
const headers = {}, key = document.querySelector('#key').value;
if (key) headers['X-API-Key'] = key;
const data = new FormData(); data.append('file',document.querySelector('#file').files[0]);
try {
const response = await fetch('/api/v1/schematics/render',{ method:'POST',headers,body:data });
const body = await response.json();
if (!response.ok) throw new Error(body.error?.message || 'Upload failed');
result.textContent = 'Queued. ';
const link = document.createElement('a'); link.href = body.viewer_url; link.textContent = 'Open schematic'; result.append(link);
} catch (e) { result.textContent = e.message; }
finally { button.disabled = false; }
});
+8
مشاهده پرونده
@@ -0,0 +1,8 @@
:root { color-scheme: light; font-family: Arial,sans-serif; }
* { box-sizing: border-box; }
html, body, #viewport { width: 100%; height: 100%; margin: 0; overflow: hidden; }
#viewport { touch-action: none; user-select: none; cursor: grab; outline: none; background: #fff; }
#viewport:active { cursor: grabbing; }
#viewport > svg { display: block; width: 100%; height: 100%; overflow: hidden; }
#state { position: fixed; left: 50%; top: 50%; transform: translate(-50%,-50%); color: #333; font-size: 14px; text-align: center; }
#state:empty { display: none; }
+2
مشاهده پرونده
@@ -0,0 +1,2 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1"><title>Schematic</title><link rel="stylesheet" href="/fonts.css"><link rel="stylesheet" href="/assets/viewer.css"><script type="module" src="/assets/viewer.js"></script></head><body><main id="viewport" aria-label="Schematic viewer" tabindex="0"></main><output id="state" role="status"></output></body></html>
+96
مشاهده پرونده
@@ -0,0 +1,96 @@
import { Camera } from './camera.js';
const viewport = document.querySelector('#viewport');
const state = document.querySelector('#state');
const id = location.pathname.split('/').pop();
const fragment = new URLSearchParams(location.hash.slice(1));
const token = fragment.get('token') || sessionStorage.getItem(`schematic:${id}`);
if (token) sessionStorage.setItem(`schematic:${id}`, token);
if (location.hash) history.replaceState(null, '', location.pathname);
const headers = token ? { 'X-Viewer-Token': token } : {};
const controller = new AbortController();
addEventListener('pagehide', () => controller.abort(), { once: true });
const base = `/api/v1/schematics/${id}`;
async function request(path, raw = false) {
const r = await fetch(base + path, { headers, signal: controller.signal, cache: 'no-store' });
if (!r.ok) { const body = await r.json(); throw new Error(body.error?.message || 'Schematic unavailable'); }
return raw ? r.text() : r.json();
}
async function start() {
state.textContent = 'Rendering…';
let result;
for (;;) {
result = await request('/status');
if (result.status === 'completed') break;
if (result.status === 'failed') throw new Error(result.error?.message || 'Rendering failed');
await new Promise(resolve => setTimeout(resolve, 1000));
}
const text = await request('/svg', true);
const parsed = new DOMParser().parseFromString(text, 'image/svg+xml');
if (parsed.querySelector('parsererror') || parsed.documentElement.localName !== 'svg') throw new Error('Invalid rendering artifact');
const svg = document.importNode(parsed.documentElement, true);
const b = svg.viewBox.baseVal;
const camera = new Camera({ x: b.x, y: b.y, width: b.width, height: b.height }, viewport.clientWidth, viewport.clientHeight);
const background = svg.querySelector('rect')?.getAttribute('fill') || '#ffffff';
// A rect spanning the camera is the original sheet color, not an added grid/UI.
const backdrop = svg.querySelector('rect');
let frame = 0;
function paint() {
if (frame) return;
frame = requestAnimationFrame(() => {
frame = 0;
svg.setAttribute('viewBox', camera.viewBox);
const [x,y,w,h] = camera.viewBox.split(' ').map(Number);
if (backdrop) for (const [key,value] of Object.entries({ x,y,width:w,height:h })) backdrop.setAttribute(key,value);
});
}
viewport.append(svg);
await document.fonts.ready;
viewport.style.backgroundColor = background;
state.textContent = '';
paint();
viewport.addEventListener('wheel', e => {
e.preventDefault();
const delta = e.deltaY * (e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? viewport.clientHeight : 1);
camera.zoom(Math.exp(-Math.max(-400, Math.min(400, delta)) * .002), e.clientX, e.clientY); paint();
}, { passive: false });
const pointers = new Map();
const metrics = () => {
const p = [...pointers.values()];
const x = p.reduce((s,p) => s+p.x,0)/p.length, y = p.reduce((s,p) => s+p.y,0)/p.length;
return { x, y, distance: p.length > 1 ? Math.hypot(p[0].x-p[1].x,p[0].y-p[1].y) : 0 };
};
viewport.addEventListener('pointerdown', e => {
if (e.button !== 0) return;
viewport.setPointerCapture(e.pointerId); pointers.set(e.pointerId, { x:e.clientX,y:e.clientY });
});
viewport.addEventListener('pointermove', e => {
if (!pointers.has(e.pointerId)) return;
const before = metrics(); pointers.set(e.pointerId,{ x:e.clientX,y:e.clientY }); const after = metrics();
if (before.distance > 0 && after.distance > 0) camera.zoom(after.distance/before.distance,before.x,before.y);
camera.pan(after.x-before.x,after.y-before.y); paint();
});
for (const type of ['pointerup','pointercancel','lostpointercapture']) viewport.addEventListener(type,e => pointers.delete(e.pointerId));
viewport.addEventListener('dblclick', () => { camera.fit(); paint(); });
viewport.addEventListener('keydown', e => {
if (e.key === 'Home' || e.key === '0') camera.fit();
else if (e.key === '+' || e.key === '=') camera.zoom(1.25,camera.width/2,camera.height/2);
else if (e.key === '-') camera.zoom(.8,camera.width/2,camera.height/2);
else if (e.key.startsWith('Arrow')) camera.pan(e.key==='ArrowLeft'?40:e.key==='ArrowRight'?-40:0,e.key==='ArrowUp'?40:e.key==='ArrowDown'?-40:0);
else return;
e.preventDefault(); paint();
});
new ResizeObserver(() => {
const center = camera.world(camera.width/2,camera.height/2);
camera.resize(viewport.clientWidth,viewport.clientHeight);
camera.fitScale = Math.min(camera.width/camera.bounds.width,camera.height/camera.bounds.height);
camera.x = camera.width/2-center.x*camera.scale; camera.y = camera.height/2-center.y*camera.scale; paint();
}).observe(viewport);
// Enforce configured expiry even when a viewer remains open.
function expire() {
const remaining = result.expires_at*1000-Date.now();
if (remaining <= 0) { viewport.replaceChildren(); state.textContent = 'This schematic link has expired'; }
else setTimeout(expire,Math.min(2147483647,remaining));
}
expire();
}
start().catch(e => { if (e.name !== 'AbortError') state.textContent = e.message; });