@ninkch/dsh-infinite-canvas 0.19.0 → 0.19.1
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/client.js +183 -14
- package/index.js +68 -4
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -654,6 +654,8 @@ window.__ModuleLoader__.load({
|
|
|
654
654
|
'dirActionNone': '当前没有选中自建动作',
|
|
655
655
|
'dirLens': '焦段(mm)',
|
|
656
656
|
'dirSensor': '传感器',
|
|
657
|
+
'mediaMissing': '素材缺失',
|
|
658
|
+
'retry': '重试',
|
|
657
659
|
'dirKeys': '快捷键',
|
|
658
660
|
'dirRenderLive': '实时渲染中 · 所见即成片',
|
|
659
661
|
'dirSun': '太阳光(物理日光)',
|
|
@@ -1548,6 +1550,8 @@ window.__ModuleLoader__.load({
|
|
|
1548
1550
|
'dirExportCancelled': 'Export cancelled',
|
|
1549
1551
|
'dirLookAhead': 'Look ahead',
|
|
1550
1552
|
'dirSensor': 'Sensor',
|
|
1553
|
+
'mediaMissing': 'Asset file missing',
|
|
1554
|
+
'retry': 'Retry',
|
|
1551
1555
|
'dirKeys': 'Shortcuts',
|
|
1552
1556
|
'dirRenderLive': 'Live render · what you see is the final',
|
|
1553
1557
|
'dirSun': 'Sun (physical daylight)',
|
|
@@ -2562,7 +2566,9 @@ body[data-ds-dark-theme] .dshic-inline-input{border-color:var(--dsw-alias-border
|
|
|
2562
2566
|
.dshic-canvas{flex:1;position:relative;overflow:hidden;background-color:var(--dsw-alias-bg-base);
|
|
2563
2567
|
background-image:radial-gradient(color-mix(in srgb,var(--dsw-alias-border-l2) 55%,transparent) 1px,transparent 1px);
|
|
2564
2568
|
outline:none;}
|
|
2565
|
-
.dshic-world{position:absolute;left:0;top:0;transform-origin:0 0;
|
|
2569
|
+
.dshic-world{position:absolute;left:0;top:0;transform-origin:0 0;}
|
|
2570
|
+
/* 仅在交互中提升图层:常驻 will-change 会把整张画布变成一张巨大纹理,页面缩放/重排时先白一片 */
|
|
2571
|
+
.dshic-world.is-interacting{will-change:transform;}
|
|
2566
2572
|
.dshic-edges{position:absolute;left:0;top:0;width:1px;height:1px;overflow:visible;pointer-events:none;}
|
|
2567
2573
|
.dshic-edge{fill:none;stroke-width:2;pointer-events:none;}
|
|
2568
2574
|
.dshic-edge.kind-text{stroke:var(--dsw-alias-label-secondary);}
|
|
@@ -2990,7 +2996,7 @@ body[data-ds-dark-theme] .dshic-storage-details{border-color:var(--dsw-alias-bor
|
|
|
2990
2996
|
.dshic-canvas.is-node-dragging .dshic-media-wrap.is-running .dshic-media-fill{animation:none !important;}
|
|
2991
2997
|
/* 视频 poster 生成失败:中性占位(不闪、不破图) */
|
|
2992
2998
|
.dshic-media-placeholder{position:absolute;inset:0;background:var(--dshic-preview-bg);display:block;}
|
|
2993
|
-
@keyframes dshic-sheen{0%{
|
|
2999
|
+
@keyframes dshic-sheen{0%{opacity:.82}50%{opacity:1}100%{opacity:.82}}
|
|
2994
3000
|
/* 操作栏(第三段,原型 .foot):固定高度 NODE_FOOT_H = 7 + 24 + 7 + 1px border-top;
|
|
2995
3001
|
border-top + 上下内距保证按钮与画面之间有明显间距(不贴边),且 flex:none 不参与画面比例 */
|
|
2996
3002
|
.dshic-media-foot{flex:none;height:var(--dshic-foot-h);box-sizing:border-box;display:flex;align-items:center;gap:6px;
|
|
@@ -4856,6 +4862,7 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
4856
4862
|
rec.rel = rel;
|
|
4857
4863
|
rec.size = blob.size;
|
|
4858
4864
|
rec.stored = 'disk';
|
|
4865
|
+
if (kind === 'image') { try { queueThumb(rec.id); } catch { /* 忽略 */ } }
|
|
4859
4866
|
const proj = S.projects.find((p) => p.id === projectId);
|
|
4860
4867
|
if (S.store === 'sqlite') {
|
|
4861
4868
|
// 主路径:文件落项目文件夹 + 元数据进 canvas.db(任意浏览器/远程立即可见)
|
|
@@ -5016,7 +5023,12 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
5016
5023
|
cv.width = w; cv.height = h;
|
|
5017
5024
|
const ctx = cv.getContext && cv.getContext('2d');
|
|
5018
5025
|
if (!ctx) return finish(null);
|
|
5019
|
-
|
|
5026
|
+
const k2 = Math.min(1, 512 / Math.max(w, h));
|
|
5027
|
+
const w2 = Math.max(1, Math.round(w * k2));
|
|
5028
|
+
const h2 = Math.max(1, Math.round(h * k2));
|
|
5029
|
+
cv.width = w2; cv.height = h2;
|
|
5030
|
+
ctx.fillStyle = '#14181f'; ctx.fillRect(0, 0, w2, h2);
|
|
5031
|
+
ctx.drawImage(v, 0, 0, w2, h2);
|
|
5020
5032
|
finish(cv.toDataURL('image/jpeg', 0.72));
|
|
5021
5033
|
} catch { finish(null); }
|
|
5022
5034
|
};
|
|
@@ -5093,6 +5105,108 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
5093
5105
|
return { poster, state };
|
|
5094
5106
|
}
|
|
5095
5107
|
|
|
5108
|
+
// ============================================================
|
|
5109
|
+
// 节点缩略图(系统派生文件,**不是资产**)
|
|
5110
|
+
// <rel>.thumb.jpg:最长边 512、JPEG 0.72;不进 assets.json / 资产库 / 导入扫描。
|
|
5111
|
+
// 节点只挂它(几十 KB);点开看原图 / 点播放才按需加载原文件(看哪个取哪个)。
|
|
5112
|
+
// ============================================================
|
|
5113
|
+
const THUMB_MAX = 512, THUMB_QUALITY = 0.72;
|
|
5114
|
+
const thumbCache = new Map();
|
|
5115
|
+
const thumbInflight = new Map();
|
|
5116
|
+
const thumbFailed = new Set(); // 负缓存:每个资产每会话只尝试一次
|
|
5117
|
+
const thumbUrlForRel = (rel) => (rel ? fileUrlFor(rel + '.thumb.jpg') : null);
|
|
5118
|
+
function thumbIsFailed(assetId) { return !!assetId && thumbFailed.has(assetId); }
|
|
5119
|
+
async function probeHeadUrl(url) {
|
|
5120
|
+
if (!url) return false;
|
|
5121
|
+
try { const r = await fetch(url, { method: 'HEAD', cache: 'no-store' }); return !!(r && r.ok); } catch { return false; }
|
|
5122
|
+
}
|
|
5123
|
+
async function makeThumbBlob(blob) {
|
|
5124
|
+
if (!blob) return null;
|
|
5125
|
+
const draw = (src, w0, h0) => {
|
|
5126
|
+
const k = Math.min(1, THUMB_MAX / Math.max(1, Math.max(w0, h0)));
|
|
5127
|
+
const w = Math.max(1, Math.round(w0 * k));
|
|
5128
|
+
const h = Math.max(1, Math.round(h0 * k));
|
|
5129
|
+
const cv = document.createElement('canvas');
|
|
5130
|
+
cv.width = w; cv.height = h;
|
|
5131
|
+
const g = cv.getContext && cv.getContext('2d');
|
|
5132
|
+
if (!g) return Promise.resolve(null);
|
|
5133
|
+
g.fillStyle = '#14181f'; g.fillRect(0, 0, w, h);
|
|
5134
|
+
g.drawImage(src, 0, 0, w, h);
|
|
5135
|
+
return new Promise((res) => { try { cv.toBlob((b) => res(b || null), 'image/jpeg', THUMB_QUALITY); } catch { res(null); } });
|
|
5136
|
+
};
|
|
5137
|
+
// createImageBitmap 在部分环境(软件渲染 / 异常 GPU)会**挂起而不抛错**:
|
|
5138
|
+
// 必须加超时竞速,否则 thumbInflight 永不清除、节点永远停在占位("一直加载")。
|
|
5139
|
+
try {
|
|
5140
|
+
if (typeof createImageBitmap === 'function') {
|
|
5141
|
+
const bmp = await Promise.race([
|
|
5142
|
+
createImageBitmap(blob),
|
|
5143
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('bitmap timeout')), 1500)),
|
|
5144
|
+
]);
|
|
5145
|
+
try { return await draw(bmp, bmp.width || 1, bmp.height || 1); }
|
|
5146
|
+
finally { try { if (bmp.close) bmp.close(); } catch { /* 忽略 */ } }
|
|
5147
|
+
}
|
|
5148
|
+
} catch { /* 超时/不支持 → 走 <img> 回退 */ }
|
|
5149
|
+
const url = URL.createObjectURL(blob);
|
|
5150
|
+
try {
|
|
5151
|
+
const img = await new Promise((res, rej) => {
|
|
5152
|
+
const im = new Image();
|
|
5153
|
+
im.onload = () => res(im); im.onerror = () => rej(new Error('decode failed')); im.src = url;
|
|
5154
|
+
});
|
|
5155
|
+
return await draw(img, img.naturalWidth || 1, img.naturalHeight || 1);
|
|
5156
|
+
} catch { return null; } finally { try { URL.revokeObjectURL(url); } catch { /* 忽略 */ } }
|
|
5157
|
+
}
|
|
5158
|
+
async function ensureThumbForAsset(assetId) {
|
|
5159
|
+
if (!assetId) return null;
|
|
5160
|
+
if (thumbCache.has(assetId)) return thumbCache.get(assetId);
|
|
5161
|
+
if (thumbInflight.has(assetId)) return thumbInflight.get(assetId);
|
|
5162
|
+
const task = (async () => {
|
|
5163
|
+
const rec = await idbGet(assetId).catch(() => null);
|
|
5164
|
+
if (!rec || !rec.rel || rec.kind === 'video') return null; // 视频走首帧 poster
|
|
5165
|
+
const url = thumbUrlForRel(rec.rel);
|
|
5166
|
+
if (await probeHeadUrl(url)) { thumbCache.set(assetId, url); return url; }
|
|
5167
|
+
if (thumbFailed.has(assetId)) return null;
|
|
5168
|
+
const blob = await assetBlob(assetId).catch(() => null);
|
|
5169
|
+
const thumb = await makeThumbBlob(blob);
|
|
5170
|
+
if (!thumb) { thumbFailed.add(assetId); return null; }
|
|
5171
|
+
try {
|
|
5172
|
+
const r = await fetch('/' + NS + '/api/thumb?rel=' + encodeURIComponent(rec.rel), {
|
|
5173
|
+
method: 'POST', headers: { 'content-type': 'image/jpeg' }, body: thumb,
|
|
5174
|
+
});
|
|
5175
|
+
if (r && r.ok) { thumbCache.set(assetId, url); return url; }
|
|
5176
|
+
} catch { /* 落盘失败 → 内存兜底 */ }
|
|
5177
|
+
const objUrl = URL.createObjectURL(thumb);
|
|
5178
|
+
thumbCache.set(assetId, objUrl);
|
|
5179
|
+
return objUrl;
|
|
5180
|
+
})();
|
|
5181
|
+
thumbInflight.set(assetId, task);
|
|
5182
|
+
task.then(() => thumbInflight.delete(assetId), () => thumbInflight.delete(assetId));
|
|
5183
|
+
return task;
|
|
5184
|
+
}
|
|
5185
|
+
function useNodeThumb(assetId) {
|
|
5186
|
+
const [url, setUrl] = useState(null);
|
|
5187
|
+
useEffect(() => {
|
|
5188
|
+
if (!assetId) { setUrl(null); return undefined; }
|
|
5189
|
+
let ok = true;
|
|
5190
|
+
ensureThumbForAsset(assetId).then((u) => { if (ok) setUrl(u); }).catch(() => { if (ok) setUrl(null); });
|
|
5191
|
+
return () => { ok = false; };
|
|
5192
|
+
}, [assetId]);
|
|
5193
|
+
return url;
|
|
5194
|
+
}
|
|
5195
|
+
let thumbQueueActive = 0;
|
|
5196
|
+
const thumbQueue = [];
|
|
5197
|
+
function queueThumb(assetId) {
|
|
5198
|
+
if (!assetId) return;
|
|
5199
|
+
thumbQueue.push(assetId);
|
|
5200
|
+
const pump = () => {
|
|
5201
|
+
while (thumbQueueActive < 2 && thumbQueue.length) {
|
|
5202
|
+
const id = thumbQueue.shift();
|
|
5203
|
+
thumbQueueActive++;
|
|
5204
|
+
ensureThumbForAsset(id).catch(() => null).finally(() => { thumbQueueActive--; pump(); });
|
|
5205
|
+
}
|
|
5206
|
+
};
|
|
5207
|
+
pump();
|
|
5208
|
+
}
|
|
5209
|
+
|
|
5096
5210
|
// ============================================================
|
|
5097
5211
|
// 生成能力:演示模式(本地合成)+ 自定义 API
|
|
5098
5212
|
// ============================================================
|
|
@@ -8404,6 +8518,7 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
8404
8518
|
rec.rel = rel;
|
|
8405
8519
|
rec.size = blob.size;
|
|
8406
8520
|
rec.stored = 'disk';
|
|
8521
|
+
if (kind === 'image') { try { queueThumb(rec.id); } catch { /* 忽略 */ } }
|
|
8407
8522
|
const proj = S.projects.find((p) => p.id === projectId);
|
|
8408
8523
|
if (S.store === 'sqlite') {
|
|
8409
8524
|
const saved = await hostRpc('assetCreate', { asset: assetIndexRecord(rec), projectName: (proj && proj.name) || '' });
|
|
@@ -9837,6 +9952,7 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
9837
9952
|
}
|
|
9838
9953
|
|
|
9839
9954
|
function onCanvasPointerDown(e) {
|
|
9955
|
+
try { if (worldEl) worldEl.classList.add('is-interacting'); } catch { /* 忽略 */ }
|
|
9840
9956
|
const target = e.target;
|
|
9841
9957
|
// 生成面板是浮层(不在 world 内):面板内的点击不能触发画布平移/清空选择
|
|
9842
9958
|
if (target.closest && target.closest('.dshic-composer')) return;
|
|
@@ -10061,6 +10177,7 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
10061
10177
|
}
|
|
10062
10178
|
}
|
|
10063
10179
|
function onCanvasPointerUp(e) {
|
|
10180
|
+
try { if (worldEl) worldEl.classList.remove('is-interacting'); } catch { /* 忽略 */ }
|
|
10064
10181
|
if (!drag.mode) return;
|
|
10065
10182
|
const mode = drag.mode;
|
|
10066
10183
|
// 收尾:先补上最后一帧拖拽(rAF 尚未执行的 pointermove),清掉视觉 class 与 Alt 标记
|
|
@@ -11641,7 +11758,9 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
11641
11758
|
})();
|
|
11642
11759
|
return () => { ok = false; };
|
|
11643
11760
|
}, [n.id, S.assetsTick, S.store]);
|
|
11644
|
-
|
|
11761
|
+
// 节点只挂派生缩略图(几十 KB);原图/原视频仅在「点开看 / 点播放」时按需取,且一次只取一个
|
|
11762
|
+
const thumbUrlV = useNodeThumb(kind === 'image' ? n.data.assetId : null);
|
|
11763
|
+
const [playUrl, setPlayUrl] = useState(null); // 视频原文件 URL:只在播放期间存在
|
|
11645
11764
|
// 视频默认只画静态 poster;<video> 只在播放时挂载(见下方播放状态机)
|
|
11646
11765
|
const videoPoster = useVideoPoster(kind === 'video' ? n.data.assetId : null);
|
|
11647
11766
|
const posterUrl = videoPoster.poster;
|
|
@@ -11667,7 +11786,7 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
11667
11786
|
const refEdges = S.graph.edges.filter((x) => x.toNode === n.id && x.toPort === 'ref');
|
|
11668
11787
|
const firstRefSrc = refEdges.length ? S.graph.nodes[refEdges[0].fromNode] : null;
|
|
11669
11788
|
const firstRefAid = firstRefSrc ? servedAssetId(firstRefSrc, refEdges[0].fromPort) : null;
|
|
11670
|
-
const refThumbUrl =
|
|
11789
|
+
const refThumbUrl = useNodeThumb(firstRefAid);
|
|
11671
11790
|
const running = n.data.status === 'running';
|
|
11672
11791
|
const done = !!n.data.assetId;
|
|
11673
11792
|
const statusLabel = running ? T('genRunning') : done ? T('generated') : (n.data.status === 'error' ? T('genFailed') : '');
|
|
@@ -11691,7 +11810,8 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
11691
11810
|
normalizeMediaNodeSize(nd);
|
|
11692
11811
|
});
|
|
11693
11812
|
};
|
|
11694
|
-
const imgShown = done && !!
|
|
11813
|
+
const imgShown = done && !!thumbUrlV && kind === 'image' && mediaState !== 'error';
|
|
11814
|
+
const imgMissing = done && kind === 'image' && !thumbUrlV && thumbIsFailed(n.data.assetId);
|
|
11695
11815
|
const videoPlaying = done && kind === 'video' && playing;
|
|
11696
11816
|
// 视频:有 poster 即视为就绪(mediaState ready 由 poster 的 onLoad 设置,不再依赖 <video> 元数据)
|
|
11697
11817
|
const posterLoading = done && kind === 'video' && !posterUrl && (posterState === 'idle' || posterState === 'loading');
|
|
@@ -11721,7 +11841,14 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
11721
11841
|
const v = videoRef.current;
|
|
11722
11842
|
if (!v) return;
|
|
11723
11843
|
try { const p = v.play(); if (p && typeof p.catch === 'function') p.catch(() => { /* 保留播放键 */ }); } catch { /* 忽略 */ }
|
|
11724
|
-
}, [playing, n.data.assetId]);
|
|
11844
|
+
}, [playing, n.data.assetId, playUrl]);
|
|
11845
|
+
// 播放时才去取原视频 URL(懒加载:看哪个取哪个;暂停/结束立即释放)
|
|
11846
|
+
useEffect(() => {
|
|
11847
|
+
if (!videoPlaying) { setPlayUrl(null); return undefined; }
|
|
11848
|
+
let ok = true;
|
|
11849
|
+
assetUrl(n.data.assetId).then((u) => { if (ok) setPlayUrl(u); }).catch(() => { if (ok) setPlayUrl(null); });
|
|
11850
|
+
return () => { ok = false; };
|
|
11851
|
+
}, [videoPlaying, n.data.assetId]);
|
|
11725
11852
|
/** 进度条拖动 seek(控件条只在播放中出现,拖动期间不打断播放)。 */
|
|
11726
11853
|
const seekVideo = (t) => {
|
|
11727
11854
|
const v = videoRef.current;
|
|
@@ -11781,19 +11908,39 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
11781
11908
|
posterFailed
|
|
11782
11909
|
? h('div', { key: 'poster-fail', className: 'dshic-media-placeholder', 'aria-hidden': 'true' })
|
|
11783
11910
|
: null,
|
|
11911
|
+
// 原文件缺失(404):稳定占位 + 重试,不再每次渲染都请求
|
|
11912
|
+
imgMissing
|
|
11913
|
+
? h('div', { key: 'missing', className: 'dshic-media-placeholder is-missing' },
|
|
11914
|
+
h('span', null, T('mediaMissing')),
|
|
11915
|
+
h('button', {
|
|
11916
|
+
type: 'button', className: 'dshic-mini is-ghost',
|
|
11917
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
11918
|
+
onClick: (e) => {
|
|
11919
|
+
e.stopPropagation();
|
|
11920
|
+
thumbFailed.delete(n.data.assetId);
|
|
11921
|
+
setMediaState('idle');
|
|
11922
|
+
update((s2) => { s2.assetsTick++; });
|
|
11923
|
+
},
|
|
11924
|
+
}, T('retry')))
|
|
11925
|
+
: null,
|
|
11784
11926
|
imgShown
|
|
11785
11927
|
? h('img', {
|
|
11786
|
-
key: 'img', className: 'dshic-media-fill', src:
|
|
11928
|
+
key: 'img', className: 'dshic-media-fill', src: thumbUrlV, draggable: false, decoding: 'async',
|
|
11787
11929
|
onLoad: (e) => { setMediaState('ready'); const el = e && e.currentTarget; recordMediaDims(el && el.naturalWidth, el && el.naturalHeight); },
|
|
11788
11930
|
onError: () => setMediaState('error'),
|
|
11789
|
-
onDoubleClick: (e) => {
|
|
11931
|
+
onDoubleClick: async (e) => {
|
|
11932
|
+
e.stopPropagation();
|
|
11933
|
+
// 双击放大:**此时才**取原图(单张、按需;关闭灯箱即卸载)
|
|
11934
|
+
const u = await assetUrl(n.data.assetId).catch(() => null);
|
|
11935
|
+
if (u) openLightbox('image', u, n.data.label);
|
|
11936
|
+
},
|
|
11790
11937
|
})
|
|
11791
11938
|
: null,
|
|
11792
11939
|
// 播放时才挂真实 <video>(poster 作为 poster 属性兜底,挂载瞬间也不闪)
|
|
11793
11940
|
videoPlaying
|
|
11794
11941
|
? h('video', {
|
|
11795
|
-
key: 'vid', ref: videoRef, className: 'dshic-media-fill', src:
|
|
11796
|
-
poster: posterUrl || undefined, preload: '
|
|
11942
|
+
key: 'vid', ref: videoRef, className: 'dshic-media-fill', src: playUrl || undefined,
|
|
11943
|
+
poster: posterUrl || undefined, preload: 'auto', autoPlay: true,
|
|
11797
11944
|
// 视频控件保留 stopPropagation(拖动/选择不被视频吃掉),单击画面打开生成面板;
|
|
11798
11945
|
// 双击画面 = 播放/暂停(原型行为);播放键自身另行 stopPropagation。
|
|
11799
11946
|
onPointerDown: (e) => { e.stopPropagation(); },
|
|
@@ -11866,9 +12013,12 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
11866
12013
|
done
|
|
11867
12014
|
? h('div', { key: 'acts', className: 'dshic-media-foot', 'data-node-foot': '1' },
|
|
11868
12015
|
h('button', {
|
|
11869
|
-
type: 'button', className: 'dshic-mini is-ghost', disabled: !
|
|
12016
|
+
type: 'button', className: 'dshic-mini is-ghost', disabled: !done,
|
|
11870
12017
|
onPointerDown: (e) => e.stopPropagation(),
|
|
11871
|
-
onClick: async () =>
|
|
12018
|
+
onClick: async () => {
|
|
12019
|
+
const u = await assetUrl(n.data.assetId).catch(() => null);
|
|
12020
|
+
if (u) downloadBlobFromUrl(u, await downloadNameForAsset(n.data.assetId, (kind === 'image' ? 'image' : 'video') + '-' + n.id.slice(-5), kind));
|
|
12021
|
+
},
|
|
11872
12022
|
}, Ic('download', 11), T('download')),
|
|
11873
12023
|
h('button', {
|
|
11874
12024
|
type: 'button', className: 'dshic-mini is-ghost', disabled: running,
|
|
@@ -13393,6 +13543,9 @@ const DIRECTOR_KEY_HELP = [
|
|
|
13393
13543
|
// 任何「不影响 scene 但影响画面」的变化(切模式/选中/悬停/显示开关)都必须 bumpView(),
|
|
13394
13544
|
// 渲染 effect 只认这个 revision → 不再出现「切了模式要等点击画面才刷新」。
|
|
13395
13545
|
const canvasRef = useRef(null);
|
|
13546
|
+
const dirWheelHandlerRef = useRef(null); // 最新的滚轮处理器(原生监听转发用)
|
|
13547
|
+
const dirViewportElRef = useRef(null);
|
|
13548
|
+
const dirWheelListenerRef = useRef(null);
|
|
13396
13549
|
const pipRef = useRef(null);
|
|
13397
13550
|
const pipGlRef = useRef(null);
|
|
13398
13551
|
const glRef = useRef(null);
|
|
@@ -14910,7 +15063,23 @@ const DIRECTOR_KEY_HELP = [
|
|
|
14910
15063
|
h('main', { className: 'dshic-dir-stage' },
|
|
14911
15064
|
h('div', {
|
|
14912
15065
|
className: cls('dshic-dir-viewport', S.directorPendingPlace && 'is-placing', S.directorDragging && 'is-dragging'), 'data-role': 'dir-viewport',
|
|
14913
|
-
onPointerDown: onDown, onPointerMove: onMove, onPointerUp: onUp, onPointerLeave: onUp,
|
|
15066
|
+
onPointerDown: onDown, onPointerMove: onMove, onPointerUp: onUp, onPointerLeave: onUp,
|
|
15067
|
+
// 原生 non-passive 滚轮:React 的 onWheel 无法 preventDefault(页面会一起缩放)
|
|
15068
|
+
ref: (el) => {
|
|
15069
|
+
dirWheelHandlerRef.current = onWheel;
|
|
15070
|
+
const prev = dirViewportElRef.current;
|
|
15071
|
+
if (prev && prev !== el && dirWheelListenerRef.current) {
|
|
15072
|
+
try { prev.removeEventListener('wheel', dirWheelListenerRef.current); } catch { /* 忽略 */ }
|
|
15073
|
+
}
|
|
15074
|
+
dirViewportElRef.current = el;
|
|
15075
|
+
if (!dirWheelListenerRef.current) {
|
|
15076
|
+
dirWheelListenerRef.current = (ev) => { const fn = dirWheelHandlerRef.current; if (fn) fn(ev); };
|
|
15077
|
+
}
|
|
15078
|
+
if (el) {
|
|
15079
|
+
try { el.removeEventListener('wheel', dirWheelListenerRef.current); } catch { /* 忽略 */ }
|
|
15080
|
+
try { el.addEventListener('wheel', dirWheelListenerRef.current, { passive: false }); } catch { /* 忽略 */ }
|
|
15081
|
+
}
|
|
15082
|
+
},
|
|
14914
15083
|
onContextMenu: (e2) => { if (e2 && e2.preventDefault) e2.preventDefault(); },
|
|
14915
15084
|
},
|
|
14916
15085
|
h('canvas', { ref: canvasRef, className: 'dshic-dir-canvas' }),
|
package/index.js
CHANGED
|
@@ -421,6 +421,10 @@ function dbAssetDelete(id) {
|
|
|
421
421
|
const mapped = physicalRel(row.rel) || row.rel;
|
|
422
422
|
const { abs } = safeJoin(root, mapped);
|
|
423
423
|
unlinkSync(abs);
|
|
424
|
+
// 派生预览文件随原文件一起删(不存在则忽略)
|
|
425
|
+
for (const suffix of ['.thumb.jpg', '.poster.jpg']) {
|
|
426
|
+
try { unlinkSync(abs + suffix); } catch { /* 没有就忽略 */ }
|
|
427
|
+
}
|
|
424
428
|
} catch { /* 已不存在视为成功 */ }
|
|
425
429
|
}
|
|
426
430
|
return { ok: true, deleted: true };
|
|
@@ -880,7 +884,8 @@ function dataRootImport() {
|
|
|
880
884
|
}
|
|
881
885
|
try {
|
|
882
886
|
const list = JSON.parse(readFileSync(path.join(dir, 'assets.json'), 'utf8'));
|
|
883
|
-
|
|
887
|
+
// 派生预览文件不是资产:历史 data 里混入也要剔除
|
|
888
|
+
if (Array.isArray(list)) assets[id] = list.filter((a) => !isDerivedMedia(a && a.rel));
|
|
884
889
|
} catch { /* 无资产索引 */ }
|
|
885
890
|
// 多画布:画布文档镜像在 projects/<项目文件夹>/canvases/*.json(updatedAt 新者胜)
|
|
886
891
|
try {
|
|
@@ -975,6 +980,21 @@ function mimeOf(p) {
|
|
|
975
980
|
return m ? (MIME_BY_EXT[m[1].toLowerCase()] || 'application/octet-stream') : 'application/octet-stream';
|
|
976
981
|
}
|
|
977
982
|
|
|
983
|
+
// ============================================================
|
|
984
|
+
// 派生预览文件(系统内部用,**不是资产**)
|
|
985
|
+
// <rel>.thumb.jpg 节点展示用的极小压缩图(图片/截图/视频首帧)
|
|
986
|
+
// <rel>.poster.jpg 视频首帧(历史口径,视作同一类派生文件)
|
|
987
|
+
// 规则:不进资产库 / assets.json / 导入扫描;随原文件删除;不分配资产 ID。
|
|
988
|
+
// ============================================================
|
|
989
|
+
const THUMB_MAX_SIDE = 512; // 缩略图最长边(px)
|
|
990
|
+
function isDerivedMedia(rel) {
|
|
991
|
+
return /\.(?:thumb|poster)\.jpg$/i.test(String(rel || '').trim());
|
|
992
|
+
}
|
|
993
|
+
function thumbRelOf(rel) {
|
|
994
|
+
const t = String(rel || '').trim();
|
|
995
|
+
return t ? t + '.thumb.jpg' : null;
|
|
996
|
+
}
|
|
997
|
+
|
|
978
998
|
/**
|
|
979
999
|
* 弹出系统"选择目录"对话框(阻塞至用户选择/取消)。
|
|
980
1000
|
* Windows = PowerShell + WinForms FolderBrowserDialog(TopMost,可新建文件夹);
|
|
@@ -2775,7 +2795,7 @@ async function updateInstall() {
|
|
|
2775
2795
|
export const inject = ['webServer', 'tools'];
|
|
2776
2796
|
|
|
2777
2797
|
// 仅供诊断 / 自动化测试(不属于 RPC 面,运行时行为不受影响)
|
|
2778
|
-
export const _internals = { updateCheck, updateInstall, npmLatestVersion, cmpVer, findPnpm, getProfileDir: () => PROFILE_DIR, getVersion: () => VERSION, dataRoot, setDataRoot, dataRootMigrate, dataRootImport, ensureProjectFolder, physicalRel, mirrorProjectToDataRoot, writeAssetIndex, removeAssetIndexEntry, readDataRootMeta, removeDataRootProject, sqlDeleteProjectRows, deletedProjectIds, addDeletedProjectId, clearDeletedProjectId, fetchProxyForward, storyboardConsistencyWarnings, readAssetIndexNames, safeJoin, mimeOf, pipeFile, getIcConfigPath: () => CONFIG_PATH, canvasCreateProject, canvasWriteNode, canvasConnect, canvasDeleteProject, canvasDeleteNode, canvasRunNode, canvasNodeStatus, canvasGetBible, canvasUpdateBible, canvasCreateNode, canvasOpen, canvasesList, canvasCreate, canvasRename, canvasDelete, canvasDocId, isCanvasDocId, splitCanvasDocId, normalizeCanvasId, canvasRegistry, sanitizeCanvases, readCanvasDoc, writeCanvasDoc, deleteCanvasDocsForProject, projectsList, projectsPut, projectsGet, projectsDelete, projectsSync, sqliteAvailable: () => !!SQLITE, getDb, closeDb, dbAssetsList, dbAssetsGet, dbAssetNames, dbAssetCreate, dbAssetUpdate, dbAssetDelete, objectAbsFor, ftsReconcile, readDocOrNull, docExists, sqlActive, rpcMethods: () => _rpcMethods, canvasUpdateNode, canvasGenerateNode };
|
|
2798
|
+
export const _internals = { THUMB_MAX_SIDE, thumbRelOf, isDerivedMedia, updateCheck, updateInstall, npmLatestVersion, cmpVer, findPnpm, getProfileDir: () => PROFILE_DIR, getVersion: () => VERSION, dataRoot, setDataRoot, dataRootMigrate, dataRootImport, ensureProjectFolder, physicalRel, mirrorProjectToDataRoot, writeAssetIndex, removeAssetIndexEntry, readDataRootMeta, removeDataRootProject, sqlDeleteProjectRows, deletedProjectIds, addDeletedProjectId, clearDeletedProjectId, fetchProxyForward, storyboardConsistencyWarnings, readAssetIndexNames, safeJoin, mimeOf, pipeFile, getIcConfigPath: () => CONFIG_PATH, canvasCreateProject, canvasWriteNode, canvasConnect, canvasDeleteProject, canvasDeleteNode, canvasRunNode, canvasNodeStatus, canvasGetBible, canvasUpdateBible, canvasCreateNode, canvasOpen, canvasesList, canvasCreate, canvasRename, canvasDelete, canvasDocId, isCanvasDocId, splitCanvasDocId, normalizeCanvasId, canvasRegistry, sanitizeCanvases, readCanvasDoc, writeCanvasDoc, deleteCanvasDocsForProject, projectsList, projectsPut, projectsGet, projectsDelete, projectsSync, sqliteAvailable: () => !!SQLITE, getDb, closeDb, dbAssetsList, dbAssetsGet, dbAssetNames, dbAssetCreate, dbAssetUpdate, dbAssetDelete, objectAbsFor, ftsReconcile, readDocOrNull, docExists, sqlActive, rpcMethods: () => _rpcMethods, canvasUpdateNode, canvasGenerateNode };
|
|
2779
2799
|
|
|
2780
2800
|
function resolveModel(providers, want) {
|
|
2781
2801
|
const byProvider = want && want.provider
|
|
@@ -3926,7 +3946,8 @@ export function apply(ctx) {
|
|
|
3926
3946
|
if (!st.isFile()) { res.statusCode = 404; res.end('not found'); return; }
|
|
3927
3947
|
res.setHeader('content-type', mimeOf(abs));
|
|
3928
3948
|
res.setHeader('accept-ranges', 'bytes');
|
|
3929
|
-
|
|
3949
|
+
// 派生预览文件内容不变 → 长缓存;原文件保持 no-cache
|
|
3950
|
+
res.setHeader('cache-control', isDerivedMedia(rel) ? 'public, max-age=31536000, immutable' : 'no-cache');
|
|
3930
3951
|
// HEAD:只回应元信息(node fetch HEAD 探针用),不读文件、不占解码/内存
|
|
3931
3952
|
if (req.method === 'HEAD') {
|
|
3932
3953
|
res.statusCode = 200;
|
|
@@ -3999,7 +4020,50 @@ export function apply(ctx) {
|
|
|
3999
4020
|
}
|
|
4000
4021
|
},
|
|
4001
4022
|
}),
|
|
4002
|
-
//
|
|
4023
|
+
// 节点展示用缩略图落盘:body 为 jpeg/png,写入 `<rel>.thumb.jpg`。
|
|
4024
|
+
// 缩略图是系统派生文件:不进 assets.json、不出现在资产库;节点只加载它(几十 KB),
|
|
4025
|
+
// 点开看原图 / 点播放才加载原文件。
|
|
4026
|
+
ctx.webServer.register({
|
|
4027
|
+
kind: 'exact',
|
|
4028
|
+
path: `/${NAME}/api/thumb`,
|
|
4029
|
+
async handler(req, res) {
|
|
4030
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
4031
|
+
try {
|
|
4032
|
+
if (req.method !== 'POST' && req.method !== 'PUT') {
|
|
4033
|
+
res.statusCode = 405;
|
|
4034
|
+
res.end(JSON.stringify({ error: '仅接受 POST/PUT' }));
|
|
4035
|
+
return;
|
|
4036
|
+
}
|
|
4037
|
+
const root = dataRoot();
|
|
4038
|
+
if (!root) throw new Error('未设置数据根目录');
|
|
4039
|
+
const u = new URL(req.url, 'http://localhost');
|
|
4040
|
+
const rawRel = String(u.searchParams.get('rel') || '').trim();
|
|
4041
|
+
if (!rawRel) throw new Error('缺少 rel');
|
|
4042
|
+
if (/\.thumb\.jpg$/i.test(rawRel)) throw new Error('rel 不应包含 thumb 后缀');
|
|
4043
|
+
const ct = String(req.headers['content-type'] || '').toLowerCase();
|
|
4044
|
+
if (ct && !/^image\/(jpeg|png)/.test(ct)) throw new Error('缩略图只接受 image/jpeg 或 image/png');
|
|
4045
|
+
const rel = physicalRel(rawRel) || rawRel;
|
|
4046
|
+
const { abs, clean } = safeJoin(root, rel + '.thumb.jpg');
|
|
4047
|
+
mkdirSync(path.dirname(abs), { recursive: true });
|
|
4048
|
+
const chunks = [];
|
|
4049
|
+
let size = 0;
|
|
4050
|
+
const MAX_THUMB = 2 * 1024 * 1024;
|
|
4051
|
+
for await (const c of req) {
|
|
4052
|
+
size += c.length;
|
|
4053
|
+
if (size > MAX_THUMB) { res.statusCode = 413; res.end(JSON.stringify({ error: 'thumb 过大(上限 2MB)' })); return; }
|
|
4054
|
+
chunks.push(c);
|
|
4055
|
+
}
|
|
4056
|
+
if (!size) throw new Error('thumb 内容为空');
|
|
4057
|
+
await fs.writeFile(abs, Buffer.concat(chunks));
|
|
4058
|
+
res.end(JSON.stringify({ ok: true, rel: clean, size, maxSide: THUMB_MAX_SIDE }));
|
|
4059
|
+
} catch (e) {
|
|
4060
|
+
res.statusCode = 400;
|
|
4061
|
+
res.end(JSON.stringify({ error: String((e && e.message) || e) }));
|
|
4062
|
+
}
|
|
4063
|
+
},
|
|
4064
|
+
}),
|
|
4065
|
+
|
|
4066
|
+
// CAS 对象库读路径(0.18):0.18.0–0.18.4 上传的媒体仍在 objects/ 里,必须继续可读。
|
|
4003
4067
|
// IC-08 起写路径已退役(POST/PUT 一律 405),只有 GET。
|
|
4004
4068
|
ctx.webServer.register({
|
|
4005
4069
|
kind: 'exact',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ninkch/dsh-infinite-canvas",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Node-based short-drama studio for DeepSeek Harness: script → storyboard → assets → shots → voiced, subtitled final cut on an infinite canvas.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|