@ninkch/dsh-infinite-canvas 0.19.1 → 0.19.2
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/assets/director/engine.js +984 -39
- package/client.js +412 -76
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -632,6 +632,7 @@ window.__ModuleLoader__.load({
|
|
|
632
632
|
'dirBone': '骨骼',
|
|
633
633
|
'dirBoneRot': '骨骼旋转',
|
|
634
634
|
'dirPoseLib': '姿态库',
|
|
635
|
+
'dirCustomAction': '自定义动作',
|
|
635
636
|
'dirPoseAnimAdd': '记录姿态帧',
|
|
636
637
|
'dirPoseAnimClear': '清空姿态动画',
|
|
637
638
|
'dirPoseAnimAdded': '姿态帧已记录',
|
|
@@ -682,6 +683,46 @@ window.__ModuleLoader__.load({
|
|
|
682
683
|
'dirCurFill': '当前构图',
|
|
683
684
|
'dirAddShot': '添加镜头',
|
|
684
685
|
'dirPoseKey': '姿态关键帧',
|
|
686
|
+
'dirIk': 'IK 反向动力学',
|
|
687
|
+
'dirIkAdd': '添加 IK',
|
|
688
|
+
'dirIkChain': '骨骼链',
|
|
689
|
+
'dirIkTarget': '目标',
|
|
690
|
+
'dirIkTargetPoint': '固定点',
|
|
691
|
+
'dirIkTargetKeys': '关键帧',
|
|
692
|
+
'dirIkTakeCurrent': '取当前位置',
|
|
693
|
+
'dirIkKey': '打目标帧',
|
|
694
|
+
'dirIkPole': '极向量',
|
|
695
|
+
'dirIkEmpty': '暂无 IK',
|
|
696
|
+
'dirConstraint': '约束',
|
|
697
|
+
'dirConstraintAdd': '添加约束',
|
|
698
|
+
'dirConstraintType': '类型',
|
|
699
|
+
'dirConstraintTarget': '目标点',
|
|
700
|
+
'dirConstraintFrom': '来源骨骼',
|
|
701
|
+
'dirConstraintMix': '混合',
|
|
702
|
+
'dirConstraintLimit': '角度限制(度)',
|
|
703
|
+
'dirConstraintEmpty': '暂无约束',
|
|
704
|
+
'dirClips': '动作层',
|
|
705
|
+
'dirClipAdd': '添加动作层',
|
|
706
|
+
'dirClipBake': '烘焙动作',
|
|
707
|
+
'dirClipBvh': 'BVH 导入',
|
|
708
|
+
'dirClipBvhPh': '粘贴 BVH 文本(可选:先在外部减采样)',
|
|
709
|
+
'dirClipBlend': '混合方式',
|
|
710
|
+
'dirClipInfluence': '影响',
|
|
711
|
+
'dirClipLoop': '循环',
|
|
712
|
+
'dirClipEmpty': '暂无动作层',
|
|
713
|
+
'dirClipBaked': '已从姿态关键帧烘焙动作层',
|
|
714
|
+
'dirClipBvhImported': 'BVH 已导入为动作层',
|
|
715
|
+
'dirClipBvhFail': 'BVH 解析/重定向失败',
|
|
716
|
+
'dirClipKeys': '关键帧数',
|
|
717
|
+
'dirClipName': '名称',
|
|
718
|
+
'dirClipUp': '上移',
|
|
719
|
+
'dirClipDown': '下移',
|
|
720
|
+
'assetMigrated': '已迁移 {n} 个旧资产到数据目录',
|
|
721
|
+
'assetMigration': '资产迁移',
|
|
722
|
+
'assetMigrateNow': '立即迁移',
|
|
723
|
+
'assetMigrateDone': '已迁移 {n} · 缺失 {m}',
|
|
724
|
+
'assetMigrateNoRoot': '未设置数据根目录,暂不迁移',
|
|
725
|
+
'dirPoseKey': '姿态关键帧',
|
|
685
726
|
'dirPoseKeyCount': '姿态帧',
|
|
686
727
|
'dirPoseKeyClear2': '清除姿态帧',
|
|
687
728
|
'dirPoseKeyAdded': '姿态关键帧已记录',
|
|
@@ -701,11 +742,11 @@ window.__ModuleLoader__.load({
|
|
|
701
742
|
'dirPathNone': '无',
|
|
702
743
|
'dirPathAddPoint': '加点',
|
|
703
744
|
'dirPathPointAdded': '路径点已添加',
|
|
704
|
-
'dirPathDraw': '
|
|
745
|
+
'dirPathDraw': '画线',
|
|
705
746
|
'dirPathDrawing': '正在画(点地面)',
|
|
706
747
|
'dirPathDrawHint': '点地面添加路径点,再点按钮结束',
|
|
707
|
-
'dirPathClear': '
|
|
708
|
-
'dirPathPointDel': '
|
|
748
|
+
'dirPathClear': '清空',
|
|
749
|
+
'dirPathPointDel': '删点',
|
|
709
750
|
'dirSpecTitle': 'AI 预演规格',
|
|
710
751
|
'dirSpecShow': '规格 JSON',
|
|
711
752
|
'dirSpecCopy': '生成规格',
|
|
@@ -721,7 +762,7 @@ window.__ModuleLoader__.load({
|
|
|
721
762
|
'dirPathPointNone': '请先点选一个路径点',
|
|
722
763
|
'dirPathCleared': '路径已清除,物体已复位',
|
|
723
764
|
'dirPathCount': '路径点',
|
|
724
|
-
'dirLocomotion': '
|
|
765
|
+
'dirLocomotion': '移动驱动',
|
|
725
766
|
'dirLocoSource': '动作来源',
|
|
726
767
|
'dirLocoNone': '不驱动',
|
|
727
768
|
'dirLocoClip': '自动步频(跟随动作)',
|
|
@@ -732,7 +773,7 @@ window.__ModuleLoader__.load({
|
|
|
732
773
|
'dirLocoCycle': '循环周期(s)',
|
|
733
774
|
'dirLocoReadout': '当前',
|
|
734
775
|
'dirLocoStepsPerSec': '步/秒',
|
|
735
|
-
'dirPathHint': '
|
|
776
|
+
'dirPathHint': '沿路径运动',
|
|
736
777
|
'dirPathLine': '折线',
|
|
737
778
|
'dirPathCurve': '曲线(跟随朝向)',
|
|
738
779
|
'dirLookAhead': '前瞻距离',
|
|
@@ -1474,13 +1515,13 @@ window.__ModuleLoader__.load({
|
|
|
1474
1515
|
'dirCamAdded': 'Camera added (current view)',
|
|
1475
1516
|
'dirCamDeleted': 'Camera deleted',
|
|
1476
1517
|
'dirPathNone': 'None',
|
|
1477
|
-
'dirPathAddPoint': 'Add
|
|
1518
|
+
'dirPathAddPoint': 'Add',
|
|
1478
1519
|
'dirPathPointAdded': 'Path point added',
|
|
1479
|
-
'dirPathDraw': 'Draw
|
|
1520
|
+
'dirPathDraw': 'Draw',
|
|
1480
1521
|
'dirPathDrawing': 'Drawing (click ground)',
|
|
1481
1522
|
'dirPathDrawHint': 'Click ground to add points, click again to finish',
|
|
1482
|
-
'dirPathClear': 'Clear
|
|
1483
|
-
'dirPathPointDel': '
|
|
1523
|
+
'dirPathClear': 'Clear',
|
|
1524
|
+
'dirPathPointDel': 'Del pt',
|
|
1484
1525
|
'dirSpecTitle': 'AI stage spec',
|
|
1485
1526
|
'dirSpecShow': 'Spec JSON',
|
|
1486
1527
|
'dirSpecCopy': 'Generate spec',
|
|
@@ -1507,7 +1548,7 @@ window.__ModuleLoader__.load({
|
|
|
1507
1548
|
'dirLocoCycle': 'Cycle (s)',
|
|
1508
1549
|
'dirLocoReadout': 'Current',
|
|
1509
1550
|
'dirLocoStepsPerSec': 'steps/s',
|
|
1510
|
-
'dirPathHint': '
|
|
1551
|
+
'dirPathHint': 'Follows path',
|
|
1511
1552
|
'dirShot': 'Shot',
|
|
1512
1553
|
'dirSkeleton': 'Skeleton',
|
|
1513
1554
|
'dirMaterial': 'Material',
|
|
@@ -1522,6 +1563,7 @@ window.__ModuleLoader__.load({
|
|
|
1522
1563
|
'dirBone': 'Bone',
|
|
1523
1564
|
'dirBoneRot': 'Bone rotation',
|
|
1524
1565
|
'dirPoseLib': 'Pose library',
|
|
1566
|
+
'dirCustomAction': 'Custom action',
|
|
1525
1567
|
'dirPoseAnimAdd': 'Record pose key',
|
|
1526
1568
|
'dirPoseAnimClear': 'Clear pose anim',
|
|
1527
1569
|
'dirPoseAnimAdded': 'Pose key recorded',
|
|
@@ -1575,6 +1617,46 @@ window.__ModuleLoader__.load({
|
|
|
1575
1617
|
'dirShotApplied': 'Shot applied',
|
|
1576
1618
|
'dirCurFill': 'Framing',
|
|
1577
1619
|
'dirAddShot': 'Add shot',
|
|
1620
|
+
'dirPoseKey': 'Pose keyframe',
|
|
1621
|
+
'dirIk': 'Inverse kinematics',
|
|
1622
|
+
'dirIkAdd': 'Add IK',
|
|
1623
|
+
'dirIkChain': 'Bone chain',
|
|
1624
|
+
'dirIkTarget': 'Target',
|
|
1625
|
+
'dirIkTargetPoint': 'Fixed point',
|
|
1626
|
+
'dirIkTargetKeys': 'Keyframes',
|
|
1627
|
+
'dirIkTakeCurrent': 'Use current',
|
|
1628
|
+
'dirIkKey': 'Key target',
|
|
1629
|
+
'dirIkPole': 'Pole',
|
|
1630
|
+
'dirIkEmpty': 'No IK',
|
|
1631
|
+
'dirConstraint': 'Constraints',
|
|
1632
|
+
'dirConstraintAdd': 'Add constraint',
|
|
1633
|
+
'dirConstraintType': 'Type',
|
|
1634
|
+
'dirConstraintTarget': 'Target point',
|
|
1635
|
+
'dirConstraintFrom': 'Source bone',
|
|
1636
|
+
'dirConstraintMix': 'Mix',
|
|
1637
|
+
'dirConstraintLimit': 'Angle limits (deg)',
|
|
1638
|
+
'dirConstraintEmpty': 'No constraints',
|
|
1639
|
+
'dirClips': 'Action layers',
|
|
1640
|
+
'dirClipAdd': 'Add layer',
|
|
1641
|
+
'dirClipBake': 'Bake',
|
|
1642
|
+
'dirClipBvh': 'Import BVH',
|
|
1643
|
+
'dirClipBvhPh': 'Paste BVH text (decimate first if large)',
|
|
1644
|
+
'dirClipBlend': 'Blend',
|
|
1645
|
+
'dirClipInfluence': 'Influence',
|
|
1646
|
+
'dirClipLoop': 'Loop',
|
|
1647
|
+
'dirClipEmpty': 'No layers',
|
|
1648
|
+
'dirClipBaked': 'Action layer baked from pose keys',
|
|
1649
|
+
'dirClipBvhImported': 'BVH imported as action layer',
|
|
1650
|
+
'dirClipBvhFail': 'BVH parse/retarget failed',
|
|
1651
|
+
'dirClipKeys': 'Keys',
|
|
1652
|
+
'dirClipName': 'Name',
|
|
1653
|
+
'dirClipUp': 'Up',
|
|
1654
|
+
'dirClipDown': 'Down',
|
|
1655
|
+
'assetMigrated': 'Migrated {n} legacy assets to data root',
|
|
1656
|
+
'assetMigration': 'Asset migration',
|
|
1657
|
+
'assetMigrateNow': 'Migrate now',
|
|
1658
|
+
'assetMigrateDone': 'Migrated {n} · missing {m}',
|
|
1659
|
+
'assetMigrateNoRoot': 'No data root configured; migration deferred',
|
|
1578
1660
|
'dirPoseKey': 'Pose key',
|
|
1579
1661
|
'dirPoseKeyCount': 'Pose keys',
|
|
1580
1662
|
'dirPoseKeyClear2': 'Clear pose keys',
|
|
@@ -2418,8 +2500,8 @@ body[data-ds-dark-theme] .dshic-studio-modal{border-color:var(--dsw-alias-border
|
|
|
2418
2500
|
.dshic-dir-treeitem{height:24px;padding:0 8px;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:11.5px;text-align:left;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
2419
2501
|
.dshic-dir-treeitem:hover{background:var(--dsw-alias-interactive-bg-hover);}
|
|
2420
2502
|
.dshic-dir-treeitem.is-on{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent);color:var(--dsw-alias-brand-primary);}
|
|
2421
|
-
.dshic-dir-keyrow{display:flex;gap:6px;margin-top:6px;}
|
|
2422
|
-
.dshic-dir-mini{height:22px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;cursor:pointer;}
|
|
2503
|
+
.dshic-dir-keyrow{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;align-items:center;}
|
|
2504
|
+
.dshic-dir-mini{height:22px;min-width:0;padding:0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;cursor:pointer;white-space:nowrap;flex:none;display:inline-flex;align-items:center;justify-content:center;line-height:1;}
|
|
2423
2505
|
.dshic-dir-mini:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary);}
|
|
2424
2506
|
.dshic-dir-mini.is-danger:hover{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);}
|
|
2425
2507
|
.dshic-dir-props{flex:1;padding:0 12px 12px;display:flex;flex-direction:column;gap:6px;min-height:0;}
|
|
@@ -2431,7 +2513,9 @@ body[data-ds-dark-theme] .dshic-studio-modal{border-color:var(--dsw-alias-border
|
|
|
2431
2513
|
.dshic-dir-row{display:flex;align-items:center;gap:6px;font-size:11.5px;color:var(--dsw-alias-label-secondary);}
|
|
2432
2514
|
.dshic-dir-row>span{flex:none;width:44px;}
|
|
2433
2515
|
.dshic-dir-prompt{width:100%;box-sizing:border-box;height:96px;resize:none;padding:7px 9px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;line-height:1.5;}
|
|
2434
|
-
|
|
2516
|
+
/* 时间轴:底部固定 1/3 高度,内部滚动,不再挤压 3D 视口 */
|
|
2517
|
+
.dshic-dir-dock{flex:none;height:min(33vh,320px);max-height:320px;min-height:150px;border-top:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);overflow-x:auto;overflow-y:auto;overscroll-behavior:contain;}
|
|
2518
|
+
.dshic-dir-dockbar{position:sticky;top:0;z-index:6;}
|
|
2435
2519
|
.dshic-dir-dockbar{height:34px;display:flex;align-items:center;gap:8px;padding:0 12px;position:sticky;left:0;min-width:max-content;background:var(--dsw-alias-bg-layer-1);z-index:4;}
|
|
2436
2520
|
.dshic-dir-docktitle{font-size:11.5px;font-weight:600;color:var(--dsw-alias-label-secondary);}
|
|
2437
2521
|
.dshic-dir-time{font-size:11px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;}
|
|
@@ -2493,7 +2577,7 @@ body[data-ds-dark-theme] .dshic-studio-modal{border-color:var(--dsw-alias-border
|
|
|
2493
2577
|
.dshic-dir-posebtn{height:26px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;cursor:pointer;}
|
|
2494
2578
|
.dshic-dir-posebtn:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary);}
|
|
2495
2579
|
.dshic-dir-posebtn.is-on{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 16%,transparent);color:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);}
|
|
2496
|
-
.dshic-dir-btnrow{display:flex;gap:6px;margin:6px 0 2px;}
|
|
2580
|
+
.dshic-dir-btnrow{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0 2px;align-items:center;}
|
|
2497
2581
|
.dshic-dir-btnrow .dshic-mini{flex:1;justify-content:center;display:inline-flex;align-items:center;}
|
|
2498
2582
|
.dshic-dir-envrow{display:flex;align-items:center;gap:6px;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:5px;}
|
|
2499
2583
|
.dshic-dir-envrow>span{flex:none;width:52px;}
|
|
@@ -2502,7 +2586,7 @@ body[data-ds-dark-theme] .dshic-studio-modal{border-color:var(--dsw-alias-border
|
|
|
2502
2586
|
.dshic-dir-envrow input[type="color"]{height:24px;padding:0;border:1px solid var(--dshic-line2);border-radius:6px;background:var(--dshic-field-bg);width:100%;}
|
|
2503
2587
|
.dshic-dir-empty{font-size:11.5px;color:var(--dsw-alias-label-secondary);line-height:1.7;padding:8px 0;}
|
|
2504
2588
|
.dshic-dir-msg.is-err{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 88%,#000);}
|
|
2505
|
-
.dshic-dir-row{display:flex;align-items:center;gap:7px;flex:none;}
|
|
2589
|
+
.dshic-dir-row{display:flex;align-items:center;gap:7px;flex:none;flex-wrap:nowrap;white-space:nowrap;}
|
|
2506
2590
|
.dshic-dir-lbl{width:52px;flex:none;font-size:11.5px;color:var(--dsw-alias-label-secondary);}
|
|
2507
2591
|
.dshic-dir-num{flex:1;min-width:0;height:24px;border-radius:6px;padding:0 6px;font:inherit;font-size:12px;
|
|
2508
2592
|
border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);}
|
|
@@ -4645,7 +4729,11 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
4645
4729
|
// 服务端不可用 → 回退 IDB;服务端可用但无记录 → 迁移期只查一次(miss 后永不再查)
|
|
4646
4730
|
if (serverOk && idbMiss.has(id)) return null;
|
|
4647
4731
|
const local = await idbGetRaw(id).catch(() => null);
|
|
4648
|
-
if (local)
|
|
4732
|
+
if (local) {
|
|
4733
|
+
// 旧 IDB-only 记录:命中即后台触发一次性迁移(幂等),下次就走磁盘/SQLite
|
|
4734
|
+
if (serverOk && !local.rel && !local.sha) migrateLegacyAssets('idb-hit').catch(() => { /* 静默 */ });
|
|
4735
|
+
return local;
|
|
4736
|
+
}
|
|
4649
4737
|
if (serverOk) idbMiss.add(id);
|
|
4650
4738
|
return null;
|
|
4651
4739
|
}
|
|
@@ -4660,6 +4748,7 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
4660
4748
|
} catch { return null; }
|
|
4661
4749
|
}
|
|
4662
4750
|
async function idbDelete(id) {
|
|
4751
|
+
if (idbDeleteImpl) return idbDeleteImpl(id);
|
|
4663
4752
|
assetNameCache.delete(id);
|
|
4664
4753
|
// 释放该资产的 objectURL:否则 Blob(视频可达几十 MB)会被 URL 钉在内存里直到刷新
|
|
4665
4754
|
if (assetUrlCache.has(id)) {
|
|
@@ -4740,7 +4829,9 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
4740
4829
|
} catch { /* 忽略 */ }
|
|
4741
4830
|
return out.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
4742
4831
|
}
|
|
4832
|
+
let idbAllImpl = null; // 测试注入(迁移逻辑单测)
|
|
4743
4833
|
async function idbAll() {
|
|
4834
|
+
if (idbAllImpl) return idbAllImpl();
|
|
4744
4835
|
const db = await openDb();
|
|
4745
4836
|
return new Promise((resolve, reject) => {
|
|
4746
4837
|
const rq = db.transaction(DB_STORE, 'readonly').objectStore(DB_STORE).getAll();
|
|
@@ -4900,36 +4991,66 @@ body[data-ds-dark-theme] .dshic-pick-item.is-on{background:color-mix(in srgb,var
|
|
|
4900
4991
|
projectId: rec.projectId, projectName: (proj && proj.name) || rec.projectId, asset: assetIndexRecord(rec),
|
|
4901
4992
|
}).catch(() => { /* 索引失败不影响资产本身 */ });
|
|
4902
4993
|
}
|
|
4903
|
-
/**
|
|
4904
|
-
*
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4994
|
+
/**
|
|
4995
|
+
* 一次性迁移:把浏览器 IndexedDB 里的存量资产搬进数据目录 + SQLite,然后**清空 IDB**。
|
|
4996
|
+
* 设计要求:IndexedDB 只减不增、最终清零;本函数是唯一迁移入口,幂等且并发安全。
|
|
4997
|
+
* - 有 blob:落盘 projects/<pid>/<kind>/<file> → assetCreate(**保留原 id**)→ idbDelete
|
|
4998
|
+
* - 无 blob:记入 missing(WebView 重建后真丢失,只能重生成,不再假装能显示)
|
|
4999
|
+
* - 无 dataRoot:只记 pending,等数据根就绪后自动补跑
|
|
5000
|
+
* 触发点:启动、数据根变化、打开资产面板前、设置页手动。
|
|
5001
|
+
*/
|
|
5002
|
+
let migrateBusy = null;
|
|
5003
|
+
async function migrateLegacyAssets(reason) {
|
|
5004
|
+
if (!S.dataRoot) {
|
|
5005
|
+
update((st) => { st.assetMigration = Object.assign({}, st.assetMigration || {}, { pendingDataRoot: true, reason: reason || '' }); });
|
|
5006
|
+
return { ok: false, skipped: 'no-data-root' };
|
|
5007
|
+
}
|
|
5008
|
+
if (migrateBusy) return migrateBusy;
|
|
5009
|
+
migrateBusy = (async () => {
|
|
5010
|
+
const stat = { reason: reason || 'auto', at: Date.now(), total: 0, migrated: 0, failed: 0, missing: 0, missingIds: [] };
|
|
5011
|
+
try {
|
|
5012
|
+
const all = await idbAll();
|
|
5013
|
+
stat.total = all.length;
|
|
5014
|
+
for (const rec of all) {
|
|
5015
|
+
if (!rec || !rec.id) continue;
|
|
5016
|
+
if (rec.rel || rec.sha) continue; // 已落盘/已有 CAS 记录
|
|
5017
|
+
if (!rec.blob) {
|
|
5018
|
+
stat.missing++;
|
|
5019
|
+
if (stat.missingIds.length < 200) stat.missingIds.push(rec.id);
|
|
5020
|
+
continue;
|
|
5021
|
+
}
|
|
5022
|
+
try {
|
|
5023
|
+
const mime = rec.blob.type || rec.mime || (rec.kind === 'image' ? 'image/png' : rec.kind === 'video' ? 'video/mp4' : 'application/octet-stream');
|
|
5024
|
+
const pid = String(rec.projectId || '');
|
|
5025
|
+
if (!pid) { stat.failed++; continue; }
|
|
5026
|
+
const rel = `projects/${pid}/${rec.kind || 'image'}/${assetFileBase(rec)}.${extFromMime(mime)}`;
|
|
5027
|
+
const r = await fetch('/' + NS + '/api/asset?rel=' + encodeURIComponent(rel), { method: 'POST', body: rec.blob });
|
|
5028
|
+
if (!r.ok) { stat.failed++; continue; }
|
|
5029
|
+
const meta = { ...rec, rel, size: rec.size || rec.blob.size };
|
|
5030
|
+
delete meta.blob;
|
|
5031
|
+
meta.stored = 'disk';
|
|
5032
|
+
const proj = S.projects.find((p) => p.id === pid);
|
|
5033
|
+
await hostRpc('assetCreate', { asset: assetIndexRecord(meta), projectName: (proj && proj.name) || '' });
|
|
5034
|
+
try { await idbDelete(rec.id); } catch { /* 清理失败不阻塞 */ }
|
|
5035
|
+
if (assetMem.has(rec.id)) assetMem.set(rec.id, meta);
|
|
5036
|
+
stat.migrated++;
|
|
5037
|
+
} catch { stat.failed++; }
|
|
5038
|
+
}
|
|
5039
|
+
update((st) => { st.assetMigration = stat; if (stat.migrated) st.assetsTick++; });
|
|
5040
|
+
if (stat.migrated) toast((T('assetMigrated') || '已迁移 {n} 个旧资产到数据目录').replace('{n}', String(stat.migrated)));
|
|
5041
|
+
return { ok: true, ...stat };
|
|
5042
|
+
} catch (e) {
|
|
5043
|
+
update((st) => { st.assetMigration = Object.assign({}, stat, { error: String((e && e.message) || e) }); });
|
|
5044
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
5045
|
+
} finally {
|
|
5046
|
+
migrateBusy = null;
|
|
4930
5047
|
}
|
|
4931
|
-
}
|
|
5048
|
+
})();
|
|
5049
|
+
return migrateBusy;
|
|
4932
5050
|
}
|
|
5051
|
+
/** 兼容旧调用名(外部/测试可能引用)。 */
|
|
5052
|
+
function autoMigrateAssetsToRoot() { return migrateLegacyAssets('legacy-alias'); }
|
|
5053
|
+
|
|
4933
5054
|
function useAssetUrl(id) {
|
|
4934
5055
|
const [url, setUrl] = useState(null);
|
|
4935
5056
|
useEffect(() => {
|
|
@@ -5161,7 +5282,18 @@ async function ensureThumbForAsset(assetId) {
|
|
|
5161
5282
|
if (thumbInflight.has(assetId)) return thumbInflight.get(assetId);
|
|
5162
5283
|
const task = (async () => {
|
|
5163
5284
|
const rec = await idbGet(assetId).catch(() => null);
|
|
5164
|
-
if (!rec ||
|
|
5285
|
+
if (!rec || rec.kind === 'video') return null; // 视频走首帧 poster
|
|
5286
|
+
// 旧 IDB-only 资产(无 rel):先用 blob 生成内存缩略图保证可见,并触发一次性迁移
|
|
5287
|
+
if (!rec.rel) {
|
|
5288
|
+
if (thumbFailed.has(assetId)) return null;
|
|
5289
|
+
const legacyBlob = await assetBlob(assetId).catch(() => null);
|
|
5290
|
+
const legacyThumb = await makeThumbBlob(legacyBlob);
|
|
5291
|
+
if (!legacyThumb) { thumbFailed.add(assetId); return null; }
|
|
5292
|
+
const objUrl = URL.createObjectURL(legacyThumb);
|
|
5293
|
+
thumbCache.set(assetId, objUrl);
|
|
5294
|
+
migrateLegacyAssets('thumb-fallback').catch(() => { /* 后台自愈,失败不阻塞显示 */ });
|
|
5295
|
+
return objUrl;
|
|
5296
|
+
}
|
|
5165
5297
|
const url = thumbUrlForRel(rec.rel);
|
|
5166
5298
|
if (await probeHeadUrl(url)) { thumbCache.set(assetId, url); return url; }
|
|
5167
5299
|
if (thumbFailed.has(assetId)) return null;
|
|
@@ -7520,12 +7652,15 @@ function queueThumb(assetId) {
|
|
|
7520
7652
|
}
|
|
7521
7653
|
/** 从 Host 拉取数据根目录配置(Host 旧版无此 RPC → 标记不支持)。 */
|
|
7522
7654
|
async function refreshDataRoot() {
|
|
7655
|
+
const prevRoot = S.dataRoot;
|
|
7523
7656
|
try {
|
|
7524
7657
|
const r = await hostRpc('dataRootGet');
|
|
7525
7658
|
update((s) => { s.dataRoot = r.dataRoot || null; s.dataRootExists = !!r.exists; s.dataRootSupported = true; s.store = r.store || (r.sql ? 'sqlite' : (r.dataRoot ? 'json' : null)); });
|
|
7526
7659
|
} catch (e) {
|
|
7527
7660
|
update((s) => { s.dataRoot = null; s.store = null; s.dataRootSupported = !/未知操作/.test(String((e && e.message) || e)); });
|
|
7528
7661
|
}
|
|
7662
|
+
// 数据根从「空」变为「有」:自动补跑一次性迁移(此前因未配置被跳过)
|
|
7663
|
+
if (S.dataRoot && !prevRoot) migrateLegacyAssets('root-ready').catch(() => { /* 静默 */ });
|
|
7529
7664
|
}
|
|
7530
7665
|
// ---------- 对话 ↔ 画布同步:轮询 Host 项目索引 + 跳转请求 ----------
|
|
7531
7666
|
let hostSyncStopped = false;
|
|
@@ -13407,6 +13542,26 @@ function queueThumb(assetId) {
|
|
|
13407
13542
|
MODEL_INDEX = (j && j.models) || [];
|
|
13408
13543
|
return MODEL_INDEX;
|
|
13409
13544
|
}
|
|
13545
|
+
// 动作库(内置成熟动作 clip:three.js AnimationClip → 本地 tracks)
|
|
13546
|
+
const ACTION_DIR = 'assets/director/clips/';
|
|
13547
|
+
let ACTION_CLIPS = null;
|
|
13548
|
+
async function loadActionClips() {
|
|
13549
|
+
if (ACTION_CLIPS) return ACTION_CLIPS;
|
|
13550
|
+
try {
|
|
13551
|
+
const E2 = directorEngine();
|
|
13552
|
+
const r = await fetch(pluginAssetUrl(ACTION_DIR + 'manifest.json'));
|
|
13553
|
+
if (!r.ok) { ACTION_CLIPS = []; return ACTION_CLIPS; }
|
|
13554
|
+
const j = await r.json();
|
|
13555
|
+
const list = [];
|
|
13556
|
+
for (const it of (j && j.clips) || []) {
|
|
13557
|
+
const rr = await fetch(pluginAssetUrl(ACTION_DIR + it.file));
|
|
13558
|
+
if (!rr.ok) continue;
|
|
13559
|
+
list.push(await rr.json());
|
|
13560
|
+
}
|
|
13561
|
+
ACTION_CLIPS = E2 ? E2.decodeActionClips(list) : [];
|
|
13562
|
+
} catch { ACTION_CLIPS = []; }
|
|
13563
|
+
return ACTION_CLIPS;
|
|
13564
|
+
}
|
|
13410
13565
|
/** 按需加载一个模型并注册进引擎;返回 asset。 */
|
|
13411
13566
|
async function loadModel(id) {
|
|
13412
13567
|
const E2 = directorEngine();
|
|
@@ -13415,7 +13570,8 @@ function queueThumb(assetId) {
|
|
|
13415
13570
|
if (cached) return cached;
|
|
13416
13571
|
const r = await fetch(pluginAssetUrl(MODEL_DIR + id + '.json'));
|
|
13417
13572
|
if (!r.ok) throw new Error('模型加载失败 ' + id);
|
|
13418
|
-
const
|
|
13573
|
+
const clips = await loadActionClips().catch(() => []);
|
|
13574
|
+
const asset = E2.decodeModelPack(await r.json(), clips);
|
|
13419
13575
|
MODEL_CACHE.add(id);
|
|
13420
13576
|
return E2.registerGltfAsset(id, asset);
|
|
13421
13577
|
}
|
|
@@ -13430,6 +13586,7 @@ function queueThumb(assetId) {
|
|
|
13430
13586
|
idle: { zh: '待机', en: 'Idle' }, run: { zh: '奔跑', en: 'Run' },
|
|
13431
13587
|
walk: { zh: '行走', en: 'Walk' }, sneak_pose: { zh: '潜行', en: 'Sneak' },
|
|
13432
13588
|
jump: { zh: '跳跃', en: 'Jump' }, dance: { zh: '舞蹈', en: 'Dance' },
|
|
13589
|
+
wave: { zh: '挥手', en: 'Wave' },
|
|
13433
13590
|
death: { zh: '倒地', en: 'Death' }, hit: { zh: '受击', en: 'Hit' },
|
|
13434
13591
|
};
|
|
13435
13592
|
const clipLabel = (name) => {
|
|
@@ -13558,6 +13715,9 @@ const DIRECTOR_KEY_HELP = [
|
|
|
13558
13715
|
durRef.current = Math.max(0.5,
|
|
13559
13716
|
...(scene && Array.isArray(scene.shots) && scene.shots.length ? scene.shots.map((s2) => Number(s2.end) || 0) : [Number(scene && scene.shot && scene.shot.duration) || 6]));
|
|
13560
13717
|
const dragRef = useRef(null);
|
|
13718
|
+
const [bvhOpen, setBvhOpen] = useState(false);
|
|
13719
|
+
const [customActionOpen, setCustomActionOpen] = useState(false);
|
|
13720
|
+
const [bvhText, setBvhText] = useState('');
|
|
13561
13721
|
const fileRef = useRef(null);
|
|
13562
13722
|
const recRef = useRef(null);
|
|
13563
13723
|
const undoRef = useRef([]);
|
|
@@ -14185,7 +14345,7 @@ const DIRECTOR_KEY_HELP = [
|
|
|
14185
14345
|
patch((n) => {
|
|
14186
14346
|
const id = 'obj_' + Date.now().toString(36);
|
|
14187
14347
|
const base = { id, kind, name: p.label || p.id, pos: E.v3(Math.round(pos.x * 4) / 4, 0, Math.round(pos.z * 4) / 4), rotY: 0, visible: true };
|
|
14188
|
-
if (kind === 'mannequin') Object.assign(base, { kind: 'model', assetId: 'xbot', typeId: p.typeId, h: p.h, scale: E.v3(p.w || 1, 1, p.d || 1), clip:
|
|
14348
|
+
if (kind === 'mannequin') Object.assign(base, { kind: 'model', assetId: 'xbot', typeId: p.typeId, h: p.h, scale: E.v3(p.w || 1, 1, p.d || 1), clip: 'idle', animTime: 0, color: p.color || '' });
|
|
14189
14349
|
else if (kind === 'model') Object.assign(base, { assetId: p.id, h: p.height || 1.7, clip: '', animTime: 0, color: p.color || '' });
|
|
14190
14350
|
else if (kind === 'actor') Object.assign(base, { preset: p.id, h: 1, pose: 'stand' });
|
|
14191
14351
|
else if (kind === 'building') Object.assign(base, { preset: p.id, s: 1 });
|
|
@@ -15256,6 +15416,193 @@ const DIRECTOR_KEY_HELP = [
|
|
|
15256
15416
|
value: String(((((scene.env || {}).fog) || {}).far !== undefined ? ((scene.env || {}).fog).far : 40)),
|
|
15257
15417
|
onChange: (e2) => patch((n) => { n.env = Object.assign({}, n.env || {}, { fog: Object.assign({ enabled: true, color: '#aab4c0', near: 6, density: 1 }, (n.env || {}).fog || {}, { far: Math.max(1, Number(e2.target.value) || 40) }) }); }) })),
|
|
15258
15418
|
] : []),
|
|
15419
|
+
// ── IK / 约束 / 动作层(AI 能力对应的编辑面板)──
|
|
15420
|
+
...(sel && sel.kind === 'model' ? (() => {
|
|
15421
|
+
const asset = E.gltfAssetOf(sel.assetId);
|
|
15422
|
+
const bones = asset ? E.boneListOf(asset) : [];
|
|
15423
|
+
if (!bones.length) return [];
|
|
15424
|
+
const r = (label, node) => envRow(label, node);
|
|
15425
|
+
const boneSel = (val, onChange) => h('select', { className: 'dshic-dir-input', value: String(val || ''), onChange },
|
|
15426
|
+
[h('option', { key: '', value: '' }, '—')].concat(bones.map((b) => h('option', { key: b.index, value: b.name }, b.name))));
|
|
15427
|
+
const num = (v, on, step) => h('input', { className: 'dshic-dir-input', type: 'number', step: step || 0.01, value: String(Number(v) || 0), onChange: (e2) => on(Number(e2.target.value) || 0) });
|
|
15428
|
+
const writeObj = (fn) => patch((n) => { const o2 = n.objects.find((x) => x.id === sel.id); if (o2) fn(o2); });
|
|
15429
|
+
const ikList = Array.isArray(sel.ik) ? sel.ik : [];
|
|
15430
|
+
const conList = Array.isArray(sel.constraints) ? sel.constraints : [];
|
|
15431
|
+
const clipList = Array.isArray(sel.clips) ? sel.clips : [];
|
|
15432
|
+
const ikPos = (t) => {
|
|
15433
|
+
if (Array.isArray(t)) return t.map(Number);
|
|
15434
|
+
if (t && Array.isArray(t.keys) && t.keys.length) return (t.keys[t.keys.length - 1].pos || [0, 0, 0]).map(Number);
|
|
15435
|
+
return t ? [Number(t.x) || 0, Number(t.y) || 0, Number(t.z) || 0] : [0, 0, 0];
|
|
15436
|
+
};
|
|
15437
|
+
const endPos = (chain) => {
|
|
15438
|
+
try {
|
|
15439
|
+
const local = E.applyBonePoseToLocal(asset, playT, sel.bonePose || null, null, null, 0, { ik: sel.ik, constraints: sel.constraints, clips: sel.clips });
|
|
15440
|
+
const wm = E.localWorldMatrices(asset, local);
|
|
15441
|
+
const bn = bones.find((b) => b.name === chain[2]);
|
|
15442
|
+
if (!bn) return null;
|
|
15443
|
+
const m = wm[bn.nodeIdx];
|
|
15444
|
+
return Number.isFinite(m[12]) ? [m[12], m[13], m[14]] : null;
|
|
15445
|
+
} catch { return null; }
|
|
15446
|
+
};
|
|
15447
|
+
const actionSelect = h('div', { className: 'dshic-dir-row' }, h('span', null, T('dirAction')), h('select', {
|
|
15448
|
+
className: 'dshic-dir-input', value: sel.userActionId ? ('ua:' + sel.userActionId) : ('clip:' + String(sel.clip || ((E.gltfAssetOf(sel.assetId) || {}).animations || [{}])[0].name || '')),
|
|
15449
|
+
'data-action': 'dir-action-select',
|
|
15450
|
+
onChange: (e2) => patch((n) => { const v = String(e2.target.value || ''); const t2 = n.objects.find((x) => x.id === sel.id); if (t2) { if (v.indexOf('ua:') === 0) { const ua = E.loadUserActions(S.activeId).find((a) => a.id === v.slice(3)); if (ua) { E.applyUserAction(t2, ua); t2.userActionId = ua.id; t2.animTime = 0; } } else { const nm2 = v.slice(5); t2.clip = nm2; t2.animTime = 0; t2.userActionId = null; } } }),
|
|
15451
|
+
}, [].concat(((E.gltfAssetOf(sel.assetId) || {}).animations || []).map((a2) => h('option', { key: 'clip:' + a2.name, value: 'clip:' + a2.name }, clipLabel(a2.name))), E.loadUserActions(S.activeId).map((ua) => h('option', { key: 'ua:' + ua.id, value: 'ua:' + ua.id }, '★ ' + ua.name)))));
|
|
15452
|
+
return [
|
|
15453
|
+
h('div', { className: 'dshic-dir-subhead' }, T('dirAction')),
|
|
15454
|
+
actionSelect,
|
|
15455
|
+
h('div', { className: 'dshic-dir-btnrow' },
|
|
15456
|
+
h('button', { type: 'button', className: 'dshic-dir-mini', onClick: dupSel }, T('dirDup')),
|
|
15457
|
+
h('button', { type: 'button', className: 'dshic-dir-mini is-danger', onClick: delSel }, T('dirDel')),
|
|
15458
|
+
h('button', {
|
|
15459
|
+
type: 'button', className: 'dshic-dir-mini', 'data-action': 'dir-custom-action-toggle',
|
|
15460
|
+
onClick: () => setCustomActionOpen((v) => !v),
|
|
15461
|
+
}, (customActionOpen ? '▾ ' : '▸ ') + T('dirCustomAction'))),
|
|
15462
|
+
// ── 自定义动作(默认收起):IK / 约束 / 动作层 / 骨骼 / 保存 ──
|
|
15463
|
+
// ── 自定义动作(默认收起)──
|
|
15464
|
+
...(customActionOpen ? [
|
|
15465
|
+
h('div', { className: 'dshic-dir-subhead' }, T('dirIk')),
|
|
15466
|
+
...(ikList.length ? ikList.map((ik, ii) => {
|
|
15467
|
+
const chain = (ik.chain || []).slice(0, 3);
|
|
15468
|
+
const pos = ikPos(ik.target);
|
|
15469
|
+
const isKeys = !!(ik.target && !Array.isArray(ik.target) && Array.isArray(ik.target.keys));
|
|
15470
|
+
const upd = (fn) => writeObj((o2) => { o2.ik = JSON.parse(JSON.stringify(ikList)); fn(o2.ik[ii]); });
|
|
15471
|
+
return h('div', { key: 'ik-' + ii, className: 'dshic-dir-lightbox' },
|
|
15472
|
+
r(T('dirIkChain'), h('div', { className: 'dshic-dir-xyz' },
|
|
15473
|
+
[0, 1, 2].map((k2) => h('span', { key: k2, style: { flex: '1 1 0' } },
|
|
15474
|
+
boneSel(chain[k2], (e2) => upd((x) => { if (!Array.isArray(x.chain)) x.chain = []; x.chain[k2] = e2.target.value; })))))),
|
|
15475
|
+
r(T('dirIkTarget'), h('select', {
|
|
15476
|
+
className: 'dshic-dir-input',
|
|
15477
|
+
value: isKeys ? 'keys' : 'point',
|
|
15478
|
+
onChange: (e2) => upd((x) => {
|
|
15479
|
+
if (e2.target.value === 'keys') x.target = { keys: [{ t: Math.round(playT * 100) / 100, pos: pos.slice() }] };
|
|
15480
|
+
else x.target = pos.slice();
|
|
15481
|
+
}),
|
|
15482
|
+
}, [h('option', { key: 'point', value: 'point' }, T('dirIkTargetPoint')), h('option', { key: 'keys', value: 'keys' }, T('dirIkTargetKeys'))])),
|
|
15483
|
+
r('XYZ', h('div', { className: 'dshic-dir-xyz' },
|
|
15484
|
+
[0, 1, 2].map((k2) => h('span', { key: k2, style: { flex: '1 1 0' } }, num(pos[k2], (v) => {
|
|
15485
|
+
upd((x) => {
|
|
15486
|
+
const p2 = ikPos(x.target);
|
|
15487
|
+
p2[k2] = v;
|
|
15488
|
+
if (isKeys) x.target.keys = (x.target.keys || []).concat([{ t: Math.round(playT * 100) / 100, pos: p2 }]);
|
|
15489
|
+
else x.target = p2;
|
|
15490
|
+
});
|
|
15491
|
+
}, 0.05))))),
|
|
15492
|
+
r(T('dirIkPole'), h('div', { className: 'dshic-dir-xyz' },
|
|
15493
|
+
[0, 1, 2].map((k2) => h('span', { key: k2, style: { flex: '1 1 0' } }, num((ik.pole || [])[k2] || 0, (v) => upd((x) => {
|
|
15494
|
+
const p2 = Array.isArray(x.pole) ? x.pole.slice() : [0, 0, 0];
|
|
15495
|
+
p2[k2] = v; x.pole = p2;
|
|
15496
|
+
}), 0.05))))),
|
|
15497
|
+
h('div', { className: 'dshic-dir-keyrow' },
|
|
15498
|
+
h('button', { type: 'button', className: 'dshic-dir-mini', 'data-action': 'dir-ik-take-current', onClick: () => {
|
|
15499
|
+
const p2 = endPos(chain);
|
|
15500
|
+
if (!p2) { setMsg({ kind: 'err', text: T('dirIkEmpty') }); return; }
|
|
15501
|
+
upd((x) => {
|
|
15502
|
+
if (isKeys) x.target = { keys: (x.target.keys || []).concat([{ t: Math.round(playT * 100) / 100, pos: p2 }]) };
|
|
15503
|
+
else x.target = p2;
|
|
15504
|
+
});
|
|
15505
|
+
} }, '⌖ ' + T('dirIkTakeCurrent')),
|
|
15506
|
+
h('button', { type: 'button', className: 'dshic-dir-mini', 'data-action': 'dir-ik-key', onClick: () => {
|
|
15507
|
+
upd((x) => {
|
|
15508
|
+
if (!x.target || Array.isArray(x.target)) x.target = { keys: [{ t: 0, pos: Array.isArray(x.target) ? x.target : [0, 0, 0] }] };
|
|
15509
|
+
x.target.keys = (x.target.keys || []).concat([{ t: Math.round(playT * 100) / 100, pos: ikPos(x.target) }]);
|
|
15510
|
+
});
|
|
15511
|
+
} }, '◆ ' + T('dirIkKey')),
|
|
15512
|
+
h('button', { type: 'button', className: 'dshic-dir-mini is-danger', onClick: () => writeObj((o2) => {
|
|
15513
|
+
const arr = (o2.ik || []).slice(); arr.splice(ii, 1);
|
|
15514
|
+
if (arr.length) o2.ik = arr; else delete o2.ik;
|
|
15515
|
+
}) }, '✖')));
|
|
15516
|
+
}) : [h('div', { key: 'ik-empty', className: 'dshic-dir-time' }, T('dirIkEmpty'))]),
|
|
15517
|
+
h('div', { className: 'dshic-dir-row' },
|
|
15518
|
+
h('button', { type: 'button', className: 'dshic-dir-mini', 'data-action': 'dir-ik-add', onClick: () => writeObj((o2) => {
|
|
15519
|
+
const pick = (nm) => bones.find((b) => b.name === nm);
|
|
15520
|
+
const def = ['RightUpLeg', 'RightLeg', 'RightFoot'].map((nm) => (pick(nm) || { name: '' }).name);
|
|
15521
|
+
const chain2 = def.every((x2) => x2) ? def : bones.slice(0, 3).map((b) => b.name);
|
|
15522
|
+
const p2 = endPos(chain2) || [0, 0, 0];
|
|
15523
|
+
o2.ik = (o2.ik || []).concat([{ chain: chain2, target: p2, pole: [p2[0], p2[1] + 1, p2[2] + 1] }]);
|
|
15524
|
+
}) }, '+ ' + T('dirIkAdd'))),
|
|
15525
|
+
|
|
15526
|
+
h('div', { className: 'dshic-dir-subhead' }, T('dirConstraint')),
|
|
15527
|
+
...(conList.length ? conList.map((c, ci) => {
|
|
15528
|
+
const upd = (fn) => writeObj((o2) => { o2.constraints = JSON.parse(JSON.stringify(conList)); fn(o2.constraints[ci]); });
|
|
15529
|
+
const type = String(c.type || 'damped_track');
|
|
15530
|
+
return h('div', { key: 'con-' + ci, className: 'dshic-dir-lightbox' },
|
|
15531
|
+
r(T('dirConstraintType'), h('select', { className: 'dshic-dir-input', value: type, onChange: (e2) => upd((x) => { x.type = e2.target.value; if (x.type === 'damped_track' && !x.target) x.target = [0, 1, 0]; if (x.type === 'copy_rotation' && !x.from) x.from = bones[0].name; if (x.type === 'limit_rotation') { x.min = x.min || [-0.5, -0.5, -0.5]; x.max = x.max || [0.5, 0.5, 0.5]; } }) },
|
|
15532
|
+
[h('option', { key: 'dt', value: 'damped_track' }, 'Damped Track'), h('option', { key: 'cr', value: 'copy_rotation' }, 'Copy Rotation'), h('option', { key: 'lr', value: 'limit_rotation' }, 'Limit Rotation')])),
|
|
15533
|
+
r(T('dirBone') || 'Bone', boneSel(c.bone, (e2) => upd((x) => { x.bone = e2.target.value; }))),
|
|
15534
|
+
type === 'copy_rotation' ? r(T('dirConstraintFrom'), boneSel(c.from, (e2) => upd((x) => { x.from = e2.target.value; }))) : null,
|
|
15535
|
+
type === 'copy_rotation' ? r(T('dirConstraintMix'), num(c.mix === undefined ? 1 : c.mix, (v) => upd((x) => { x.mix = v; }), 0.05)) : null,
|
|
15536
|
+
type === 'damped_track' ? r(T('dirConstraintTarget'), h('div', { className: 'dshic-dir-xyz' },
|
|
15537
|
+
[0, 1, 2].map((k2) => h('span', { key: k2, style: { flex: '1 1 0' } }, num((Array.isArray(c.target) ? c.target : [0, 1, 0])[k2] || 0, (v) => upd((x) => {
|
|
15538
|
+
const p2 = Array.isArray(x.target) ? x.target.slice() : [0, 1, 0]; p2[k2] = v; x.target = p2;
|
|
15539
|
+
}), 0.05))))) : null,
|
|
15540
|
+
type === 'limit_rotation' ? r(T('dirConstraintLimit'), h('div', { className: 'dshic-dir-xyz' },
|
|
15541
|
+
[0, 1, 2].map((k2) => h('span', { key: k2, style: { flex: '1 1 0' } }, num(Math.round(((c.min || [])[k2] || 0) * 180 / Math.PI), (v) => upd((x) => {
|
|
15542
|
+
const mn = (x.min || [-0.5, -0.5, -0.5]).slice(); mn[k2] = (v * Math.PI) / 180; x.min = mn;
|
|
15543
|
+
}), 5))))) : null,
|
|
15544
|
+
type === 'limit_rotation' ? r('MAX', h('div', { className: 'dshic-dir-xyz' },
|
|
15545
|
+
[0, 1, 2].map((k2) => h('span', { key: k2, style: { flex: '1 1 0' } }, num(Math.round(((c.max || [])[k2] || 0) * 180 / Math.PI), (v) => upd((x) => {
|
|
15546
|
+
const mx = (x.max || [0.5, 0.5, 0.5]).slice(); mx[k2] = (v * Math.PI) / 180; x.max = mx;
|
|
15547
|
+
}), 5))))) : null,
|
|
15548
|
+
h('div', { className: 'dshic-dir-keyrow' },
|
|
15549
|
+
h('button', { type: 'button', className: 'dshic-dir-mini is-danger', onClick: () => writeObj((o2) => {
|
|
15550
|
+
const arr = (o2.constraints || []).slice(); arr.splice(ci, 1);
|
|
15551
|
+
if (arr.length) o2.constraints = arr; else delete o2.constraints;
|
|
15552
|
+
}) }, '✖')));
|
|
15553
|
+
}) : [h('div', { key: 'con-empty', className: 'dshic-dir-time' }, T('dirConstraintEmpty'))]),
|
|
15554
|
+
h('div', { className: 'dshic-dir-row' },
|
|
15555
|
+
h('button', { type: 'button', className: 'dshic-dir-mini', 'data-action': 'dir-constraint-add', onClick: () => writeObj((o2) => {
|
|
15556
|
+
o2.constraints = (o2.constraints || []).concat([{ type: 'damped_track', bone: (bones[0] || {}).name || '', target: [0, 1, 0] }]);
|
|
15557
|
+
}) }, '+ ' + T('dirConstraintAdd'))),
|
|
15558
|
+
|
|
15559
|
+
h('div', { className: 'dshic-dir-subhead' }, T('dirClips')),
|
|
15560
|
+
...(clipList.length ? clipList.map((cp, ci) => {
|
|
15561
|
+
const upd = (fn) => writeObj((o2) => { o2.clips = JSON.parse(JSON.stringify(clipList)); fn(o2.clips[ci]); });
|
|
15562
|
+
const move = (d2) => writeObj((o2) => { const arr = (o2.clips || []).slice(); const j = ci + d2; if (j < 0 || j >= arr.length) return; const t2 = arr[ci]; arr[ci] = arr[j]; arr[j] = t2; o2.clips = arr; });
|
|
15563
|
+
return h('div', { key: 'clip-' + ci, className: 'dshic-dir-lightbox' },
|
|
15564
|
+
r(T('dirClipName'), h('input', { className: 'dshic-dir-input', value: String(cp.name || ''), onChange: (e2) => upd((x) => { x.name = e2.target.value; }) })),
|
|
15565
|
+
r(T('dirClipBlend'), h('select', { className: 'dshic-dir-input', value: String(cp.blend || 'replace'), onChange: (e2) => upd((x) => { x.blend = e2.target.value; }) },
|
|
15566
|
+
[h('option', { key: 'r', value: 'replace' }, 'replace'), h('option', { key: 'a', value: 'add' }, 'add'), h('option', { key: 'm', value: 'multiply' }, 'multiply')])),
|
|
15567
|
+
r(T('dirClipInfluence'), h('input', { className: 'dshic-dir-input', type: 'range', min: 0, max: 1, step: 0.05, value: String(cp.influence === undefined ? 1 : cp.influence), onChange: (e2) => upd((x) => { x.influence = Number(e2.target.value); }) })),
|
|
15568
|
+
r(T('dirClipLoop'), h('input', { type: 'checkbox', checked: !!cp.loop, onChange: (e2) => upd((x) => { x.loop = e2.target.checked; }) })),
|
|
15569
|
+
h('div', { className: 'dshic-dir-time' }, T('dirClipKeys') + ' ' + ((cp.keys || []).length)),
|
|
15570
|
+
h('div', { className: 'dshic-dir-keyrow' },
|
|
15571
|
+
h('button', { type: 'button', className: 'dshic-dir-mini', onClick: () => move(-1) }, '↑'),
|
|
15572
|
+
h('button', { type: 'button', className: 'dshic-dir-mini', onClick: () => move(1) }, '↓'),
|
|
15573
|
+
h('button', { type: 'button', className: 'dshic-dir-mini is-danger', onClick: () => writeObj((o2) => {
|
|
15574
|
+
const arr = (o2.clips || []).slice(); arr.splice(ci, 1);
|
|
15575
|
+
if (arr.length) o2.clips = arr; else delete o2.clips;
|
|
15576
|
+
}) }, '✖')));
|
|
15577
|
+
}) : [h('div', { key: 'clip-empty', className: 'dshic-dir-time' }, T('dirClipEmpty'))]),
|
|
15578
|
+
h('div', { className: 'dshic-dir-keyrow' },
|
|
15579
|
+
h('button', { type: 'button', className: 'dshic-dir-mini', 'data-action': 'dir-clip-add', onClick: () => writeObj((o2) => {
|
|
15580
|
+
o2.clips = (o2.clips || []).concat([{ name: T('dirClips') + ' ' + ((o2.clips || []).length + 1), keys: [], blend: 'replace', influence: 1 }]);
|
|
15581
|
+
}) }, '+ ' + T('dirClipAdd')),
|
|
15582
|
+
h('button', { type: 'button', className: 'dshic-dir-mini', 'data-action': 'dir-clip-bake', onClick: () => {
|
|
15583
|
+
const keys = JSON.parse(JSON.stringify(sel.poseKeys || []));
|
|
15584
|
+
if (!keys.length) { setMsg({ kind: 'err', text: T('dirClipEmpty') }); return; }
|
|
15585
|
+
writeObj((o2) => { o2.clips = (o2.clips || []).concat([{ name: 'baked ' + ((o2.clips || []).length + 1), keys, blend: 'replace', influence: 1 }]); });
|
|
15586
|
+
setMsg({ kind: 'ok', text: T('dirClipBaked') });
|
|
15587
|
+
} }, '⇪ ' + T('dirClipBake')),
|
|
15588
|
+
h('button', { type: 'button', className: 'dshic-dir-mini', 'data-action': 'dir-bvh-import', onClick: () => setBvhOpen(!bvhOpen) }, '⚄ ' + T('dirClipBvh'))),
|
|
15589
|
+
bvhOpen ? h('div', { className: 'dshic-dir-lightbox' },
|
|
15590
|
+
h('textarea', { className: 'dshic-dir-input', style: { width: '100%', minHeight: '70px' }, placeholder: T('dirClipBvhPh'), value: bvhText, onChange: (e2) => setBvhText(e2.target.value) }),
|
|
15591
|
+
h('div', { className: 'dshic-dir-keyrow' },
|
|
15592
|
+
h('button', { type: 'button', className: 'dshic-dir-mini', onClick: () => {
|
|
15593
|
+
try {
|
|
15594
|
+
const bvh = E.parseBVH(bvhText);
|
|
15595
|
+
const rt = E.retargetBVH(asset, bvh, { sampleStep: 2 });
|
|
15596
|
+
if (!rt.poseKeys.length) throw new Error('empty');
|
|
15597
|
+
writeObj((o2) => { o2.clips = (o2.clips || []).concat([{ name: 'bvh ' + ((o2.clips || []).length + 1), keys: rt.poseKeys, blend: 'replace', influence: 1, loop: true, t0: 0, t1: rt.duration }]); });
|
|
15598
|
+
setMsg({ kind: 'ok', text: T('dirClipBvhImported') });
|
|
15599
|
+
setBvhOpen(false); setBvhText('');
|
|
15600
|
+
} catch { setMsg({ kind: 'err', text: T('dirClipBvhFail') }); }
|
|
15601
|
+
} }, '✓ ' + T('dirClipBvh')),
|
|
15602
|
+
h('button', { type: 'button', className: 'dshic-dir-mini', onClick: () => { setBvhOpen(false); setBvhText(''); } }, '✖'))) : null,
|
|
15603
|
+
] : []),
|
|
15604
|
+
];
|
|
15605
|
+
})() : []),
|
|
15259
15606
|
// ── 景深 ──
|
|
15260
15607
|
...(!sel && previzMode !== 'render' ? [
|
|
15261
15608
|
envRow(T('dirDof'), h('label', null, h('input', { type: 'checkbox', 'data-action': 'dir-dof-on', checked: !!(((scene.env || {}).dof) || {}).enabled,
|
|
@@ -15308,7 +15655,7 @@ const DIRECTOR_KEY_HELP = [
|
|
|
15308
15655
|
});
|
|
15309
15656
|
setMsg({ kind: 'ok', text: T('dirPathPointAdded') });
|
|
15310
15657
|
},
|
|
15311
|
-
},
|
|
15658
|
+
}, T('dirPathAddPoint')),
|
|
15312
15659
|
h('button', {
|
|
15313
15660
|
type: 'button', className: 'dshic-dir-mini', 'data-action': 'dir-path-draw',
|
|
15314
15661
|
onClick: () => { S.directorPathPick = !S.directorPathPick; setMsg({ kind: 'ok', text: T('dirPathDrawHint') }); bumpView(); },
|
|
@@ -15316,7 +15663,7 @@ const DIRECTOR_KEY_HELP = [
|
|
|
15316
15663
|
h('button', {
|
|
15317
15664
|
type: 'button', className: 'dshic-dir-mini is-danger', 'data-action': 'dir-path-clear',
|
|
15318
15665
|
onClick: () => { patch((n2) => { const o2 = n2.objects.find((x) => x.id === sel.id); if (!o2) return; if (o2.pathOrigin) o2.pos = { x: o2.pathOrigin.x, y: o2.pathOrigin.y, z: o2.pathOrigin.z }; o2.path = []; o2.pathMode = 'none'; }); setMsg({ kind: 'ok', text: T('dirPathCleared') }); },
|
|
15319
|
-
},
|
|
15666
|
+
}, T('dirPathClear')),
|
|
15320
15667
|
h('button', {
|
|
15321
15668
|
type: 'button', className: 'dshic-dir-mini is-danger', 'data-action': 'dir-path-del-point',
|
|
15322
15669
|
onClick: () => {
|
|
@@ -15329,7 +15676,7 @@ const DIRECTOR_KEY_HELP = [
|
|
|
15329
15676
|
S.directorPathSel = null;
|
|
15330
15677
|
setMsg({ kind: 'ok', text: T('dirPathPointDeleted') });
|
|
15331
15678
|
},
|
|
15332
|
-
},
|
|
15679
|
+
}, T('dirPathPointDel'))),
|
|
15333
15680
|
h('div', { className: 'dshic-dir-time' }, T('dirPathCount') + ' ' + ((sel.path || []).length) + ' · ' + T('dirPathHint')),
|
|
15334
15681
|
// ── 移动驱动:让动作跟着路径速度走(避免滑步)。
|
|
15335
15682
|
// 动作只在一处选:跟随上方「动作」下拉;这里只调步频参数或改用自建动作。──
|
|
@@ -15419,10 +15766,6 @@ const DIRECTOR_KEY_HELP = [
|
|
|
15419
15766
|
type: 'button', className: 'dshic-dir-mini', 'data-action': 'dir-obj-color-reset',
|
|
15420
15767
|
onClick: () => patch((n2) => { const o2 = n2.objects.find((x) => x.id === sel.id); if (o2) o2.color = ''; }),
|
|
15421
15768
|
}, T('dirReset'))),
|
|
15422
|
-
sel.kind === 'model' ? h('div', { className: 'dshic-dir-row' }, h('span', null, T('dirAction')), h('select', {
|
|
15423
|
-
className: 'dshic-dir-input', value: sel.userActionId ? ('ua:' + sel.userActionId) : ('clip:' + String(sel.clip || (E.gltfAssetOf(sel.assetId) || {}).animations && ((E.gltfAssetOf(sel.assetId) || {}).animations[0] || {}).name || '')),
|
|
15424
|
-
onChange: (e2) => patch((n) => { const v = String(e2.target.value || ''); const t2 = n.objects.find((x) => x.id === sel.id); if (t2) { if (v.indexOf('ua:') === 0) { const ua = E.loadUserActions(S.activeId).find((a) => a.id === v.slice(3)); if (ua) { E.applyUserAction(t2, ua); t2.userActionId = ua.id; } } else { t2.clip = v.replace(/^clip:/, ''); t2.userActionId = null; } }; }),
|
|
15425
|
-
}, [].concat(((E.gltfAssetOf(sel.assetId) || {}).animations || []).map((a2) => h('option', { key: 'clip:' + a2.name, value: 'clip:' + a2.name }, clipLabel(a2.name))), E.loadUserActions(S.activeId).map((ua) => h('option', { key: 'ua:' + ua.id, value: 'ua:' + ua.id }, '★ ' + ua.name))))) : null,
|
|
15426
15769
|
h('div', { className: 'dshic-dir-btnrow' },
|
|
15427
15770
|
h('button', { type: 'button', className: 'dshic-dir-mini', onClick: dupSel }, T('dirDup')),
|
|
15428
15771
|
h('button', { type: 'button', className: 'dshic-dir-mini is-danger', onClick: delSel }, T('dirDel')))) : null,
|
|
@@ -15432,7 +15775,7 @@ const DIRECTOR_KEY_HELP = [
|
|
|
15432
15775
|
if (!bones.length) return null;
|
|
15433
15776
|
const poseMap = sel.bonePose || {};
|
|
15434
15777
|
const activeBone = Number(sel.boneIndex) >= 0 ? Number(sel.boneIndex) : -1;
|
|
15435
|
-
return h('div', null,
|
|
15778
|
+
return customActionOpen ? h('div', null,
|
|
15436
15779
|
h('div', { className: 'dshic-dir-subhead' }, T('dirSkeleton') + ' (' + bones.length + ')'),
|
|
15437
15780
|
h('div', { className: 'dshic-dir-row' },
|
|
15438
15781
|
h('span', null, T('dirBone')),
|
|
@@ -15457,23 +15800,6 @@ const DIRECTOR_KEY_HELP = [
|
|
|
15457
15800
|
o2.bonePose[activeBone] = cur;
|
|
15458
15801
|
}),
|
|
15459
15802
|
})))),
|
|
15460
|
-
// ── 基础姿态(骨架级,方向式)──
|
|
15461
|
-
h('div', { className: 'dshic-dir-subhead' }, T('dirPoseLib')),
|
|
15462
|
-
h('div', { className: 'dshic-dir-posegrid' },
|
|
15463
|
-
E.BONE_POSES.filter((p) => p.id !== 'none').map((p) => h('button', {
|
|
15464
|
-
key: p.id, type: 'button',
|
|
15465
|
-
className: cls('dshic-dir-posebtn', sel.poseId === p.id && 'is-on'),
|
|
15466
|
-
'data-pose': p.id,
|
|
15467
|
-
onClick: () => {
|
|
15468
|
-
patch((n) => {
|
|
15469
|
-
const o2 = n.objects.find((x) => x.id === sel.id);
|
|
15470
|
-
if (!o2) return;
|
|
15471
|
-
o2.bonePose = {}; // 姿态由 poseId 在蒙皮时按当前动画姿势求解
|
|
15472
|
-
o2.poseId = p.id;
|
|
15473
|
-
});
|
|
15474
|
-
setMsg({ kind: 'ok', text: T('dirPoseApplied') + ' · ' + (effLang() === 'en' ? p.en : p.zh) });
|
|
15475
|
-
},
|
|
15476
|
-
}, effLang() === 'en' ? p.en : p.zh))),
|
|
15477
15803
|
// ── 用户自建动作:骨骼 + 姿态调好后保存,保存后出现在上方「动作」下拉 ──
|
|
15478
15804
|
h('div', { className: 'dshic-dir-row' },
|
|
15479
15805
|
h('input', {
|
|
@@ -15509,7 +15835,7 @@ const DIRECTOR_KEY_HELP = [
|
|
|
15509
15835
|
const o2 = n.objects.find((x) => x.id === sel.id);
|
|
15510
15836
|
if (o2) { o2.bonePose = {}; o2.poseId = null; o2.poseAnim = []; o2.poseKeys = []; o2.userActionId = null; }
|
|
15511
15837
|
}),
|
|
15512
|
-
}, T('dirPoseClear'))));
|
|
15838
|
+
}, T('dirPoseClear')))) : null;
|
|
15513
15839
|
})() : null,
|
|
15514
15840
|
...(!sel && previzMode !== 'render' ? [
|
|
15515
15841
|
h('div', { className: 'dshic-dir-subhead' }, T('dirShot')),
|
|
@@ -17297,7 +17623,7 @@ const DIRECTOR_KEY_HELP = [
|
|
|
17297
17623
|
await refreshDataRoot();
|
|
17298
17624
|
toast(T('dataRootSaved') + (S.dataRoot ? ':' + S.dataRoot : ''));
|
|
17299
17625
|
if (same) { setBusy(false); return; } // 同目录重复保存:不重复迁移
|
|
17300
|
-
|
|
17626
|
+
migrateLegacyAssets('data-root'); // 换了目录:一次性迁移旧 blob(v1 旧布局由 Host 侧自动迁移)
|
|
17301
17627
|
assetMem.clear();
|
|
17302
17628
|
update((s) => { s.assetsTick++; });
|
|
17303
17629
|
} catch (e) { toast('设置失败:' + ((e && e.message) || e)); }
|
|
@@ -17310,7 +17636,7 @@ const DIRECTOR_KEY_HELP = [
|
|
|
17310
17636
|
setVal(S.dataRoot || root);
|
|
17311
17637
|
toast(T('dataRootSaved') + ':' + S.dataRoot);
|
|
17312
17638
|
if (same) return; // 同目录:不重复迁移
|
|
17313
|
-
|
|
17639
|
+
migrateLegacyAssets('data-root'); // 换了目录:一次性迁移旧 blob
|
|
17314
17640
|
assetMem.clear();
|
|
17315
17641
|
update((s) => { s.assetsTick++; });
|
|
17316
17642
|
};
|
|
@@ -18338,7 +18664,7 @@ const DIRECTOR_KEY_HELP = [
|
|
|
18338
18664
|
// 检查更新(Host RPC;Host 旧版未带此方法时静默)
|
|
18339
18665
|
checkUpdate(true);
|
|
18340
18666
|
// 数据根目录(Host 旧版静默降级为 IndexedDB 模式);就绪后把本地遗留 blob 搬进服务端(v1 旧布局由 Host 侧自动迁移)
|
|
18341
|
-
refreshDataRoot().then(() =>
|
|
18667
|
+
refreshDataRoot().then(() => migrateLegacyAssets('boot')).catch(() => { /* 静默 */ });
|
|
18342
18668
|
// 对话 ↔ 画布同步(启动一次 + 3s 轮询;旧 Host 自动停轮询)
|
|
18343
18669
|
hostSyncTick().then(() => pushAllProjectsToHost());
|
|
18344
18670
|
const hostSyncTimer = setInterval(() => hostSyncTick(), 3000);
|
|
@@ -18463,6 +18789,16 @@ const DIRECTOR_KEY_HELP = [
|
|
|
18463
18789
|
setCanvasElForTest: (el) => { canvasEl = el; },
|
|
18464
18790
|
edgePath,
|
|
18465
18791
|
};
|
|
18792
|
+
// 旧 IDB 资产一次性迁移的测试缝(供 scripts/test-asset-migration.mjs 驱动真实迁移逻辑)
|
|
18793
|
+
window.__DSH_IC_MIGRATE__ = {
|
|
18794
|
+
migrateLegacyAssets,
|
|
18795
|
+
ensureThumbForAsset,
|
|
18796
|
+
state: () => S.assetMigration || null,
|
|
18797
|
+
setIdbAllForTest: (fn) => { idbAllImpl = fn || null; },
|
|
18798
|
+
setIdbDeleteForTest: (fn) => { idbDeleteImpl = fn || null; },
|
|
18799
|
+
setDataRootForTest: (v) => { S.dataRoot = v || null; },
|
|
18800
|
+
thumbCache, thumbFailed,
|
|
18801
|
+
};
|
|
18466
18802
|
// 排版纯函数/常量出口:预览区比例、节点外框公式、等比缩放、面板宽度(供 scripts/test-node-layout.mjs 断言)
|
|
18467
18803
|
window.__DSH_IC_LAYOUT__ = {
|
|
18468
18804
|
NODE_W, NODE_H, NODE_HDR_H, NODE_FOOT_H, MEDIA_PREVIEW_SHORT, MEDIA_MAX_LONG,
|