termdock 1.4.92 → 1.4.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{ModelPreview-DCSVK39N.js → ModelPreview-jeIaVlHs.js} +23 -23
- package/dist/client/assets/{abnfDiagram-VRR7QNED-Cac0W6zk.js → abnfDiagram-VRR7QNED-m0kMAGbl.js} +1 -1
- package/dist/client/assets/{arc-BAzeAgv3.js → arc-BBEmZHTt.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-B60pc1IP.js → architectureDiagram-ZJ3FMSHR-CfHdu6lz.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-CTelvuLg.js → blockDiagram-677ZJIJ3-D3vj1T47.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-BSZpIJI6.js → c4Diagram-LMCZKHZV-DHU5v4jr.js} +1 -1
- package/dist/client/assets/channel-CxPe7M8J.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-rVsN-eBz.js → chunk-2Q5K7J3B-vCnFRNMe.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-CbNSO3Wd.js → chunk-32BRIVSS-B_WfFTx2.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-CUgLgCDa.js → chunk-5VM5RSS4-CpnPZiTy.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-DCDiAU9I.js → chunk-EX3LRPZG-CqMYQJYq.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-Dy4swQtR.js → chunk-JWPE2WC7-C93KaEVZ.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-CBy1m0-Z.js → chunk-MOJQB5TN-Dpf2DIjh.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-DvsTnWWN.js → chunk-RYQCIY6F-Ciht2xA6.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-CDxVHUrw.js → chunk-V7JOEXUC-DDG8TvvU.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-DGpBeuzs.js → chunk-VR4S4FIN-d5vk2sQT.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-CVarUP0O.js → chunk-XXDRQBXY-VEM92fqC.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-Bck779Ff.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-Bck779Ff.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-DMMvJlzV.js → cose-bilkent-JH36ORCC-B5xb6Hgp.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-D3tKARgm.js → cynefin-VYW2F7L2-BbQuCHIA.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-eeL23tlz.js → cynefinDiagram-TSTJHNR4-DT7vda0m.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-CCqTux28.js → dagre-VKFMJZFB-DN_RFM2k.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-CBso-uTT.js → diagram-FQU43EPY-BUVDv1zf.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-DP_pPh32.js → diagram-G47NLZAW-CkbtYupE.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-bKVQgq_U.js → diagram-NH7WQ7WH-CUFWCugw.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-C-1A55G5.js → diagram-OA4YK3LP-B-8rnCCP.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-DT1l1sM-.js → diagram-WEI45ONY-CLsUSVqK.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-R9ZByLYy.js → ebnfDiagram-CCIWWBDH-v1uXp7Sp.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-DeCTwaBU.js → erDiagram-Q63AITRT-erVLfhsH.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-rWhFq4Xo.js → flowDiagram-23GEKE2U-Douvp51k.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-BucSWjmC.js → ganttDiagram-NO4QXBWP-CNzo-zqc.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-CuzMKTOB.js → gitGraphDiagram-IHSO6WYX-dFpGWjyJ.js} +1 -1
- package/dist/client/assets/index-DzN5zpis.js +451 -0
- package/dist/client/assets/index-z_B5xcqX.css +1 -0
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-B5Kpu4c6.js → infoDiagram-FWYZ7A6U-CYr8XvIj.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-vxMKPIQf.js → ishikawaDiagram-FXEZZL3T-CH3CmN-T.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-BHP1UTjd.js → journeyDiagram-5HDEW3XC-mLYKN8kx.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-Ba6Mi9zF.js → kanban-definition-HUTT4EX6-CeQS-upr.js} +1 -1
- package/dist/client/assets/{linear-P4pmxAYp.js → linear-CNjmpDQb.js} +1 -1
- package/dist/client/assets/{mermaid.core-6cbfSb9q.js → mermaid.core--hV7lBKs.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-B8uHpXag.js → mindmap-definition-LN4V7U3C-DMH5nfrP.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-ChUgCLQr.js → pegDiagram-2B236MQR-DqF2VabL.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-8IPHMbgG.js → pieDiagram-ENE6RG2P-N83WJa4m.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-CRUVv5d3.js → quadrantDiagram-ABIIQ3AL-Wp_XsS3J.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-Bthth-RW.js → railroadDiagram-RFXS5EU6-Br_j0vNZ.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-Bx_W6_7h.js → requirementDiagram-TGXJPOKE-wBLgs38K.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-65l86jKp.js → sankeyDiagram-HTMAVEWB-CaUU4xu6.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-StYQQxa2.js → sequenceDiagram-DBY2YBRQ-CvFWkRYE.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-CgQsBCMZ.js → sizeCapture-X5ZJPWSS-BLj48_Q6.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-Wu7Lnskc.js → stateDiagram-2N3HPSRC-IsEAShyE.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BaTMLGtM.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-jJ_0wxJN.js → swimlanes-5IMT3BWC-BnyDDEf1.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-DpSyuWMv.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6--4TlbNhr.js → timeline-definition-FHXFAJF6-Cy4QT6yt.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-CRL1_ae1.js → vennDiagram-L72KCM5P-DFkSBIF9.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-RnPb__te.js → wardleyDiagram-EHGQE667-BPblwS19.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-DNN8_irv.js → xychartDiagram-FW5EYKEG-DU9c9mXe.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/entry.js +8 -4
- package/dist/server/routes/filesystem.js +207 -0
- package/dist/server/utils/edaPreviewCache.js +49 -0
- package/dist/server/utils/kicadBoardInspection.js +140 -0
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
- package/dist/client/assets/channel-C-v7eWaN.js +0 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-CwRQhTCb.js +0 -1
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-CwRQhTCb.js +0 -1
- package/dist/client/assets/index-BWem2AZx.css +0 -1
- package/dist/client/assets/index-oKZ6p6xu.js +0 -450
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-Daqm2r2g.js +0 -1
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-CZ1fJU5d.js +0 -8
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import { execFile, spawn } from 'child_process';
|
|
6
7
|
import watcher from '@parcel/watcher';
|
|
@@ -14,6 +15,8 @@ import { getLanIPv4Addresses } from '../utils/localAccess.js';
|
|
|
14
15
|
import { inspectBinaryFile } from '../utils/binaryFile.js';
|
|
15
16
|
import { GitApplyError, HUNK_APPLY_MODES, runGitApply, validateHunkPatch } from '../utils/hunkApply.js';
|
|
16
17
|
import { deleteFilesystemFile } from '../utils/deleteFilesystemFile.js';
|
|
18
|
+
import { inspectKicadBoardPoint } from '../utils/kicadBoardInspection.js';
|
|
19
|
+
import { EdaPreviewCache, requestAcceptsEtag } from '../utils/edaPreviewCache.js';
|
|
17
20
|
const router = Router();
|
|
18
21
|
const MAX_FILE_SIZE = 1024 * 1024; // 1MB
|
|
19
22
|
const MAX_IMAGE_PREVIEW_SIZE = 20 * 1024 * 1024; // 20MB
|
|
@@ -43,6 +46,8 @@ const MAX_NESTED_GIT_REPOS = 32;
|
|
|
43
46
|
const NESTED_GIT_DISCOVERY_TIMEOUT_MS = 1_000;
|
|
44
47
|
const NESTED_GIT_DISCOVERY_CACHE_TTL_MS = 60_000;
|
|
45
48
|
const FS_ROUTE_TIMEOUT_MS = 6_000;
|
|
49
|
+
const EDA_PREVIEW_TIMEOUT_MS = 30_000;
|
|
50
|
+
const EDA_PREVIEW_CACHE_MAX_BYTES = 64 * 1024 * 1024;
|
|
46
51
|
const GIT_ROUTE_TIMEOUT_MS = 8_000;
|
|
47
52
|
const GIT_FILE_DIFF_ROUTE_TIMEOUT_MS = 45_000;
|
|
48
53
|
const GIT_ACTION_TIMEOUT_MS = 10 * 60_000;
|
|
@@ -2758,6 +2763,208 @@ router.get('/git-blob', async (req, res) => {
|
|
|
2758
2763
|
res.status(error instanceof OperationTimeoutError ? 504 : 200).json({ path: requestedPath ?? null, content: '', ...payload });
|
|
2759
2764
|
}
|
|
2760
2765
|
});
|
|
2766
|
+
const EDA_PREVIEW_VIEWS = new Set(['schematic', 'pcb-front', 'pcb-back', 'pcb-3d']);
|
|
2767
|
+
const edaPreviewCache = new EdaPreviewCache(EDA_PREVIEW_CACHE_MAX_BYTES);
|
|
2768
|
+
function runKicadCli(args, cwd, signal) {
|
|
2769
|
+
return new Promise((resolve, reject) => {
|
|
2770
|
+
let settled = false;
|
|
2771
|
+
const proc = execFile('kicad-cli', args, { cwd, maxBuffer: 4 * 1024 * 1024 }, (error, _stdout, stderr) => {
|
|
2772
|
+
if (settled)
|
|
2773
|
+
return;
|
|
2774
|
+
settled = true;
|
|
2775
|
+
clearTimeout(timer);
|
|
2776
|
+
signal.removeEventListener('abort', abortHandler);
|
|
2777
|
+
if (error) {
|
|
2778
|
+
reject(new Error(stderr.trim() || error.message));
|
|
2779
|
+
}
|
|
2780
|
+
else {
|
|
2781
|
+
resolve();
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
const timer = setTimeout(() => {
|
|
2785
|
+
if (settled)
|
|
2786
|
+
return;
|
|
2787
|
+
settled = true;
|
|
2788
|
+
proc.kill();
|
|
2789
|
+
signal.removeEventListener('abort', abortHandler);
|
|
2790
|
+
reject(new OperationTimeoutError('KiCad preview rendering timed out.', 'EDA_PREVIEW_TIMEOUT'));
|
|
2791
|
+
}, EDA_PREVIEW_TIMEOUT_MS);
|
|
2792
|
+
const abortHandler = () => {
|
|
2793
|
+
if (settled)
|
|
2794
|
+
return;
|
|
2795
|
+
settled = true;
|
|
2796
|
+
clearTimeout(timer);
|
|
2797
|
+
proc.kill();
|
|
2798
|
+
reject(signal.reason instanceof Error ? signal.reason : new SupersededRequestError('fs.eda-preview'));
|
|
2799
|
+
};
|
|
2800
|
+
if (signal.aborted)
|
|
2801
|
+
abortHandler();
|
|
2802
|
+
else
|
|
2803
|
+
signal.addEventListener('abort', abortHandler, { once: true });
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
// Render native KiCad sources for Review. The source path is passed as an
|
|
2807
|
+
// execFile argument (never through a shell), and every output lives in a
|
|
2808
|
+
// request-scoped temporary directory which is deleted after the response.
|
|
2809
|
+
router.get('/eda-preview', async (req, res) => {
|
|
2810
|
+
const requestId = ++fsIoRequestSeq;
|
|
2811
|
+
const startedAt = Date.now();
|
|
2812
|
+
const requestedPath = req.query.path;
|
|
2813
|
+
const requestedView = req.query.view;
|
|
2814
|
+
const action = getRequestAction(req, 'view_file');
|
|
2815
|
+
const requestSlotId = typeof req.query.requestSlotId === 'string' ? req.query.requestSlotId : undefined;
|
|
2816
|
+
const controller = new AbortController();
|
|
2817
|
+
const abortRequest = () => {
|
|
2818
|
+
if (!res.writableEnded)
|
|
2819
|
+
controller.abort(new SupersededRequestError('fs.eda-preview'));
|
|
2820
|
+
};
|
|
2821
|
+
req.on('aborted', abortRequest);
|
|
2822
|
+
res.on('close', abortRequest);
|
|
2823
|
+
registerIoSlot({ requestId, op: 'fs.eda-preview', action, slotId: requestSlotId, controller, path: requestedPath });
|
|
2824
|
+
let tempDir = null;
|
|
2825
|
+
try {
|
|
2826
|
+
if (!requestedPath || !EDA_PREVIEW_VIEWS.has(requestedView)) {
|
|
2827
|
+
res.status(400).json({ error: 'Missing path or invalid KiCad preview view' });
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
const resolvedPath = await pathValidator.validatePathAsync(requestedPath);
|
|
2831
|
+
const stat = await fs.promises.stat(resolvedPath);
|
|
2832
|
+
if (!stat.isFile())
|
|
2833
|
+
throw new Error('Path is not a file');
|
|
2834
|
+
const lowerPath = resolvedPath.toLowerCase();
|
|
2835
|
+
const isSchematic = lowerPath.endsWith('.kicad_sch');
|
|
2836
|
+
const isBoard = lowerPath.endsWith('.kicad_pcb');
|
|
2837
|
+
if (!isSchematic && !isBoard) {
|
|
2838
|
+
res.status(415).json({ error: 'Only .kicad_sch and .kicad_pcb previews are supported' });
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
if ((isSchematic && requestedView !== 'schematic') || (isBoard && requestedView === 'schematic')) {
|
|
2842
|
+
res.status(400).json({ error: 'Preview view does not match the KiCad file type' });
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
const versionKey = `${resolvedPath}\0${requestedView}\0${stat.size}\0${stat.mtimeMs}`;
|
|
2846
|
+
const etag = `"eda-${crypto.createHash('sha256').update(versionKey).digest('base64url').slice(0, 24)}"`;
|
|
2847
|
+
res.setHeader('ETag', etag);
|
|
2848
|
+
res.setHeader('Last-Modified', stat.mtime.toUTCString());
|
|
2849
|
+
res.setHeader('Cache-Control', 'private, no-cache');
|
|
2850
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
2851
|
+
res.setHeader('X-Eda-View', requestedView);
|
|
2852
|
+
if (requestAcceptsEtag(req.headers['if-none-match'], etag)) {
|
|
2853
|
+
res.setHeader('X-Eda-Cache', 'revalidated');
|
|
2854
|
+
res.status(304).end();
|
|
2855
|
+
logFsIo({ id: requestId, action, op: 'fs.eda-preview', startedAt, status: 'ok', path: resolvedPath, bytes: 0, extra: { view: requestedView, requestSlotId, cache: 'revalidated' } });
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
const cached = edaPreviewCache.get(versionKey);
|
|
2859
|
+
if (cached) {
|
|
2860
|
+
res.setHeader('Content-Type', cached.mimeType);
|
|
2861
|
+
res.setHeader('Content-Length', String(cached.body.length));
|
|
2862
|
+
res.setHeader('X-Eda-Cache', 'hit');
|
|
2863
|
+
res.send(cached.body);
|
|
2864
|
+
logFsIo({ id: requestId, action, op: 'fs.eda-preview', startedAt, status: 'ok', path: resolvedPath, bytes: cached.body.length, extra: { view: requestedView, requestSlotId, cache: 'hit' } });
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'termdock-eda-preview-'));
|
|
2868
|
+
let outputPath;
|
|
2869
|
+
let mimeType;
|
|
2870
|
+
if (isSchematic) {
|
|
2871
|
+
await runKicadCli([
|
|
2872
|
+
'sch', 'export', 'svg', '--output', tempDir, '--pages', '1',
|
|
2873
|
+
'--exclude-drawing-sheet', '--no-background-color', resolvedPath,
|
|
2874
|
+
], path.dirname(resolvedPath), controller.signal);
|
|
2875
|
+
const svgFiles = (await fs.promises.readdir(tempDir)).filter((name) => name.toLowerCase().endsWith('.svg')).sort();
|
|
2876
|
+
if (svgFiles.length === 0)
|
|
2877
|
+
throw new Error('KiCad did not produce a schematic SVG');
|
|
2878
|
+
outputPath = path.join(tempDir, svgFiles[0]);
|
|
2879
|
+
mimeType = 'image/svg+xml';
|
|
2880
|
+
}
|
|
2881
|
+
else if (requestedView === 'pcb-3d') {
|
|
2882
|
+
outputPath = path.join(tempDir, 'pcb-3d.glb');
|
|
2883
|
+
await runKicadCli([
|
|
2884
|
+
'pcb', 'export', 'glb', '--output', outputPath, '--force',
|
|
2885
|
+
'--include-tracks', '--include-pads', '--include-silkscreen', '--include-soldermask',
|
|
2886
|
+
resolvedPath,
|
|
2887
|
+
], path.dirname(resolvedPath), controller.signal);
|
|
2888
|
+
mimeType = 'model/gltf-binary';
|
|
2889
|
+
}
|
|
2890
|
+
else {
|
|
2891
|
+
const front = requestedView === 'pcb-front';
|
|
2892
|
+
outputPath = path.join(tempDir, front ? 'pcb-front.svg' : 'pcb-back.svg');
|
|
2893
|
+
const layers = front
|
|
2894
|
+
? 'F.Cu,F.Mask,F.Silkscreen,Edge.Cuts'
|
|
2895
|
+
: 'B.Cu,B.Mask,B.Silkscreen,Edge.Cuts';
|
|
2896
|
+
const args = [
|
|
2897
|
+
'pcb', 'export', 'svg', '--output', outputPath, '--mode-single', '--layers', layers,
|
|
2898
|
+
'--page-size-mode', '2', '--fit-page-to-board', '--exclude-drawing-sheet',
|
|
2899
|
+
];
|
|
2900
|
+
if (!front)
|
|
2901
|
+
args.push('--mirror');
|
|
2902
|
+
args.push(resolvedPath);
|
|
2903
|
+
await runKicadCli(args, path.dirname(resolvedPath), controller.signal);
|
|
2904
|
+
mimeType = 'image/svg+xml';
|
|
2905
|
+
}
|
|
2906
|
+
const output = await fs.promises.readFile(outputPath);
|
|
2907
|
+
if (output.length > MAX_IMAGE_PREVIEW_SIZE) {
|
|
2908
|
+
res.status(413).json({ error: 'Rendered KiCad preview is too large' });
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
edaPreviewCache.set(versionKey, { body: output, mimeType });
|
|
2912
|
+
res.setHeader('Content-Type', mimeType);
|
|
2913
|
+
res.setHeader('Content-Length', String(output.length));
|
|
2914
|
+
res.setHeader('X-Eda-Cache', 'miss');
|
|
2915
|
+
res.send(output);
|
|
2916
|
+
logFsIo({ id: requestId, action, op: 'fs.eda-preview', startedAt, status: 'ok', path: resolvedPath, bytes: output.length, extra: { view: requestedView, requestSlotId, cache: 'miss' } });
|
|
2917
|
+
}
|
|
2918
|
+
catch (error) {
|
|
2919
|
+
const payload = getErrorPayload(error);
|
|
2920
|
+
if (!res.headersSent) {
|
|
2921
|
+
res.status(error instanceof OperationTimeoutError ? 504 : 403).json(payload);
|
|
2922
|
+
}
|
|
2923
|
+
logFsIo({ id: requestId, action, op: 'fs.eda-preview', startedAt, status: 'error', path: requestedPath, code: payload.code, error: payload.error, extra: { view: requestedView, requestSlotId } });
|
|
2924
|
+
}
|
|
2925
|
+
finally {
|
|
2926
|
+
releaseIoSlot(requestSlotId, requestId);
|
|
2927
|
+
if (tempDir)
|
|
2928
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
2929
|
+
}
|
|
2930
|
+
});
|
|
2931
|
+
router.get('/eda-inspect', async (req, res) => {
|
|
2932
|
+
const requestedPath = req.query.path;
|
|
2933
|
+
const requestedView = req.query.view;
|
|
2934
|
+
const xPercent = Number(req.query.x);
|
|
2935
|
+
const yPercent = Number(req.query.y);
|
|
2936
|
+
const controller = new AbortController();
|
|
2937
|
+
const timeout = setTimeout(() => controller.abort(new OperationTimeoutError('KiCad point inspection timed out.', 'EDA_INSPECT_TIMEOUT')), EDA_PREVIEW_TIMEOUT_MS);
|
|
2938
|
+
const abortRequest = () => {
|
|
2939
|
+
if (!res.writableEnded)
|
|
2940
|
+
controller.abort(new SupersededRequestError('fs.eda-inspect'));
|
|
2941
|
+
};
|
|
2942
|
+
req.on('aborted', abortRequest);
|
|
2943
|
+
res.on('close', abortRequest);
|
|
2944
|
+
try {
|
|
2945
|
+
if (!requestedPath || !EDA_PREVIEW_VIEWS.has(requestedView)
|
|
2946
|
+
|| !Number.isFinite(xPercent) || !Number.isFinite(yPercent)
|
|
2947
|
+
|| xPercent < 0 || xPercent > 100 || yPercent < 0 || yPercent > 100) {
|
|
2948
|
+
res.status(400).json({ error: 'Missing path, view, or valid preview coordinates' });
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
const resolvedPath = await pathValidator.validatePathAsync(requestedPath);
|
|
2952
|
+
if (!resolvedPath.toLowerCase().endsWith('.kicad_pcb')) {
|
|
2953
|
+
res.json({ available: false, reason: '只有 PCB 文件支持工程坐标' });
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
const result = await inspectKicadBoardPoint(resolvedPath, requestedView, xPercent, yPercent, controller.signal);
|
|
2957
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
2958
|
+
res.json(result);
|
|
2959
|
+
}
|
|
2960
|
+
catch (error) {
|
|
2961
|
+
if (!res.headersSent)
|
|
2962
|
+
res.status(error instanceof OperationTimeoutError ? 504 : 500).json(getErrorPayload(error));
|
|
2963
|
+
}
|
|
2964
|
+
finally {
|
|
2965
|
+
clearTimeout(timeout);
|
|
2966
|
+
}
|
|
2967
|
+
});
|
|
2761
2968
|
// Stream supported image files for the right sidebar preview.
|
|
2762
2969
|
router.get('/blob', async (req, res) => {
|
|
2763
2970
|
const requestId = ++fsIoRequestSeq;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Small byte-bounded LRU for expensive KiCad render results. */
|
|
2
|
+
export class EdaPreviewCache {
|
|
3
|
+
maxBytes;
|
|
4
|
+
entries = new Map();
|
|
5
|
+
totalBytes = 0;
|
|
6
|
+
constructor(maxBytes) {
|
|
7
|
+
this.maxBytes = maxBytes;
|
|
8
|
+
}
|
|
9
|
+
get(key) {
|
|
10
|
+
const entry = this.entries.get(key);
|
|
11
|
+
if (!entry)
|
|
12
|
+
return undefined;
|
|
13
|
+
this.entries.delete(key);
|
|
14
|
+
this.entries.set(key, entry);
|
|
15
|
+
return entry;
|
|
16
|
+
}
|
|
17
|
+
set(key, entry) {
|
|
18
|
+
const previous = this.entries.get(key);
|
|
19
|
+
if (previous) {
|
|
20
|
+
this.totalBytes -= previous.body.length;
|
|
21
|
+
this.entries.delete(key);
|
|
22
|
+
}
|
|
23
|
+
if (entry.body.length > this.maxBytes)
|
|
24
|
+
return;
|
|
25
|
+
this.entries.set(key, entry);
|
|
26
|
+
this.totalBytes += entry.body.length;
|
|
27
|
+
while (this.totalBytes > this.maxBytes) {
|
|
28
|
+
const oldestKey = this.entries.keys().next().value;
|
|
29
|
+
if (oldestKey === undefined)
|
|
30
|
+
break;
|
|
31
|
+
const oldest = this.entries.get(oldestKey);
|
|
32
|
+
this.entries.delete(oldestKey);
|
|
33
|
+
if (oldest)
|
|
34
|
+
this.totalBytes -= oldest.body.length;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
get sizeBytes() {
|
|
38
|
+
return this.totalBytes;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function requestAcceptsEtag(header, etag) {
|
|
42
|
+
if (!header)
|
|
43
|
+
return false;
|
|
44
|
+
const target = etag.replace(/^W\//, '');
|
|
45
|
+
return (Array.isArray(header) ? header : [header]).some((value) => (value.split(',').some((candidate) => {
|
|
46
|
+
const token = candidate.trim();
|
|
47
|
+
return token === '*' || token.replace(/^W\//, '') === target;
|
|
48
|
+
})));
|
|
49
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
const KICAD_INSPECTION_SCRIPT = String.raw `
|
|
3
|
+
import json, math, sys
|
|
4
|
+
import pcbnew
|
|
5
|
+
|
|
6
|
+
board_path, view, x_text, y_text = sys.argv[1:5]
|
|
7
|
+
x_percent = float(x_text)
|
|
8
|
+
y_percent = float(y_text)
|
|
9
|
+
|
|
10
|
+
if view not in ('pcb-front', 'pcb-back'):
|
|
11
|
+
print(json.dumps({'available': False, 'reason': '该视图不支持精确 PCB 坐标'}))
|
|
12
|
+
raise SystemExit(0)
|
|
13
|
+
|
|
14
|
+
board = pcbnew.LoadBoard(board_path)
|
|
15
|
+
box = board.GetBoardEdgesBoundingBox()
|
|
16
|
+
left = pcbnew.ToMM(box.GetLeft())
|
|
17
|
+
top = pcbnew.ToMM(box.GetTop())
|
|
18
|
+
width = pcbnew.ToMM(box.GetWidth())
|
|
19
|
+
height = pcbnew.ToMM(box.GetHeight())
|
|
20
|
+
source_x_percent = 100.0 - x_percent if view == 'pcb-back' else x_percent
|
|
21
|
+
x_mm = left + width * source_x_percent / 100.0
|
|
22
|
+
y_mm = top + height * y_percent / 100.0
|
|
23
|
+
visible_layer_id = pcbnew.F_Cu if view == 'pcb-front' else pcbnew.B_Cu
|
|
24
|
+
visible_layer = board.GetLayerName(visible_layer_id)
|
|
25
|
+
candidates = []
|
|
26
|
+
|
|
27
|
+
def distance(x, y):
|
|
28
|
+
return math.hypot(x - x_mm, y - y_mm)
|
|
29
|
+
|
|
30
|
+
def add(kind, label, layer, x, y, reference=None, pad=None, net=None):
|
|
31
|
+
item = {
|
|
32
|
+
'kind': kind,
|
|
33
|
+
'label': label,
|
|
34
|
+
'layer': layer,
|
|
35
|
+
'distanceMm': distance(x, y),
|
|
36
|
+
}
|
|
37
|
+
if reference:
|
|
38
|
+
item['reference'] = reference
|
|
39
|
+
if pad:
|
|
40
|
+
item['pad'] = pad
|
|
41
|
+
if net:
|
|
42
|
+
item['net'] = net
|
|
43
|
+
candidates.append(item)
|
|
44
|
+
|
|
45
|
+
for footprint in board.GetFootprints():
|
|
46
|
+
reference = footprint.GetReference()
|
|
47
|
+
value = footprint.GetValue()
|
|
48
|
+
fp_layer = footprint.GetLayerName()
|
|
49
|
+
if footprint.GetLayer() == visible_layer_id:
|
|
50
|
+
pos = footprint.GetPosition()
|
|
51
|
+
add('footprint', reference + (f' ({value})' if value else ''), fp_layer,
|
|
52
|
+
pcbnew.ToMM(pos.x), pcbnew.ToMM(pos.y), reference=reference)
|
|
53
|
+
for pad_item in footprint.Pads():
|
|
54
|
+
if not pad_item.IsOnLayer(visible_layer_id):
|
|
55
|
+
continue
|
|
56
|
+
pos = pad_item.GetPosition()
|
|
57
|
+
pad_number = pad_item.GetNumber()
|
|
58
|
+
net_name = pad_item.GetNetname()
|
|
59
|
+
label = reference + (f' 焊盘 {pad_number}' if pad_number else ' 焊盘')
|
|
60
|
+
add('pad', label, visible_layer, pcbnew.ToMM(pos.x), pcbnew.ToMM(pos.y),
|
|
61
|
+
reference=reference, pad=pad_number or None, net=net_name or None)
|
|
62
|
+
|
|
63
|
+
def segment_distance(px, py, ax, ay, bx, by):
|
|
64
|
+
dx, dy = bx - ax, by - ay
|
|
65
|
+
if dx == 0 and dy == 0:
|
|
66
|
+
return math.hypot(px - ax, py - ay)
|
|
67
|
+
t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy)))
|
|
68
|
+
return math.hypot(px - (ax + t * dx), py - (ay + t * dy))
|
|
69
|
+
|
|
70
|
+
for track in board.GetTracks():
|
|
71
|
+
if isinstance(track, pcbnew.PCB_VIA):
|
|
72
|
+
if not track.IsOnLayer(visible_layer_id):
|
|
73
|
+
continue
|
|
74
|
+
pos = track.GetPosition()
|
|
75
|
+
net_name = track.GetNetname()
|
|
76
|
+
add('via', '过孔', 'F.Cu↔B.Cu', pcbnew.ToMM(pos.x), pcbnew.ToMM(pos.y), net=net_name or None)
|
|
77
|
+
continue
|
|
78
|
+
if track.GetLayer() != visible_layer_id:
|
|
79
|
+
continue
|
|
80
|
+
start, end = track.GetStart(), track.GetEnd()
|
|
81
|
+
ax, ay = pcbnew.ToMM(start.x), pcbnew.ToMM(start.y)
|
|
82
|
+
bx, by = pcbnew.ToMM(end.x), pcbnew.ToMM(end.y)
|
|
83
|
+
net_name = track.GetNetname()
|
|
84
|
+
item = {
|
|
85
|
+
'kind': 'track',
|
|
86
|
+
'label': '走线',
|
|
87
|
+
'layer': track.GetLayerName(),
|
|
88
|
+
'distanceMm': segment_distance(x_mm, y_mm, ax, ay, bx, by),
|
|
89
|
+
}
|
|
90
|
+
if net_name:
|
|
91
|
+
item['net'] = net_name
|
|
92
|
+
candidates.append(item)
|
|
93
|
+
|
|
94
|
+
candidates.sort(key=lambda item: item['distanceMm'])
|
|
95
|
+
nearest = candidates[0] if candidates else None
|
|
96
|
+
if nearest:
|
|
97
|
+
nearest['distanceMm'] = round(nearest['distanceMm'], 3)
|
|
98
|
+
|
|
99
|
+
result = {
|
|
100
|
+
'available': True,
|
|
101
|
+
'xMm': round(x_mm, 3),
|
|
102
|
+
'yMm': round(y_mm, 3),
|
|
103
|
+
'layer': visible_layer,
|
|
104
|
+
}
|
|
105
|
+
if nearest:
|
|
106
|
+
result['nearest'] = nearest
|
|
107
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
108
|
+
`;
|
|
109
|
+
export function inspectKicadBoardPoint(boardPath, view, xPercent, yPercent, signal) {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
let settled = false;
|
|
112
|
+
const child = execFile('python3', ['-c', KICAD_INSPECTION_SCRIPT, boardPath, view, String(xPercent), String(yPercent)], { maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
113
|
+
if (settled)
|
|
114
|
+
return;
|
|
115
|
+
settled = true;
|
|
116
|
+
signal.removeEventListener('abort', abortHandler);
|
|
117
|
+
if (error) {
|
|
118
|
+
reject(new Error(stderr.trim() || error.message));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
resolve(JSON.parse(stdout.trim()));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
reject(new Error('KiCad point inspection returned invalid JSON'));
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
const abortHandler = () => {
|
|
129
|
+
if (settled)
|
|
130
|
+
return;
|
|
131
|
+
settled = true;
|
|
132
|
+
child.kill();
|
|
133
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error('KiCad point inspection aborted'));
|
|
134
|
+
};
|
|
135
|
+
if (signal.aborted)
|
|
136
|
+
abortHandler();
|
|
137
|
+
else
|
|
138
|
+
signal.addEventListener('abort', abortHandler, { once: true });
|
|
139
|
+
});
|
|
140
|
+
}
|
package/package.json
CHANGED
package/runtime-manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "termdock",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.94",
|
|
5
5
|
"runtimeProtocolVersion": 1,
|
|
6
6
|
"minimumDesktopVersion": "1.4.46",
|
|
7
7
|
"nodeMajor": 22,
|
|
8
8
|
"dependencyHash": "sha256-emuXnf9w0kJF3ksMJVvdI0u8oLHKwZJ2/OzHBAXulZs=",
|
|
9
|
-
"serverBundleHash": "sha256-
|
|
9
|
+
"serverBundleHash": "sha256-Cxmc1ULHs9sunGqn3hhWQ1LifuY3eZTGro5l9nnY+ds=",
|
|
10
10
|
"entrypoint": "dist/server/cli.js",
|
|
11
11
|
"clientEntrypoint": "dist/client/index.html"
|
|
12
12
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{U as a,C as n}from"./mermaid.core-6cbfSb9q.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-CDxVHUrw.js";import{b as i}from"./mermaid.core-6cbfSb9q.js";import"./chunk-5VM5RSS4-CUgLgCDa.js";import"./chunk-XXDRQBXY-CVarUP0O.js";import"./chunk-VR4S4FIN-DGpBeuzs.js";import"./chunk-32BRIVSS-CbNSO3Wd.js";import"./purify.es-CC4Brkr_.js";import"./index-oKZ6p6xu.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-CDxVHUrw.js";import{b as i}from"./mermaid.core-6cbfSb9q.js";import"./chunk-5VM5RSS4-CUgLgCDa.js";import"./chunk-XXDRQBXY-CVarUP0O.js";import"./chunk-VR4S4FIN-DGpBeuzs.js";import"./chunk-32BRIVSS-CbNSO3Wd.js";import"./purify.es-CC4Brkr_.js";import"./index-oKZ6p6xu.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};
|