three-base 2.0.172 → 2.0.180

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.
Files changed (127) hide show
  1. package/FPScontrol.d.ts +15 -24
  2. package/FPScontrol.js +63 -87
  3. package/FPScontrol.ts +64 -88
  4. package/Ground1/Ground1.d.ts +4 -2
  5. package/Ground1/Ground1.js +77 -57
  6. package/Ground1/Ground1.ts +77 -57
  7. package/Mirror.js +0 -4
  8. package/Mirror.ts +0 -4
  9. package/MktMesh/MktMesh.d.ts +3 -2
  10. package/MktMesh/MktMesh.js +27 -8
  11. package/MktMesh/MktMesh.ts +24 -8
  12. package/MktMesh/boxLine2.d.ts +42 -0
  13. package/MktMesh/boxLine2.js +185 -0
  14. package/MktMesh/boxLine2.ts +222 -0
  15. package/MktMesh/imagePlane.d.ts +60 -0
  16. package/MktMesh/imagePlane.js +158 -0
  17. package/MktMesh/imagePlane.ts +203 -0
  18. package/MktMesh/imageTileLoader.d.ts +19 -0
  19. package/MktMesh/imageTileLoader.js +55 -0
  20. package/MktMesh/imageTileLoader.ts +64 -0
  21. package/MktMesh/mktMappingShader.d.ts +6 -5
  22. package/MktMesh/mktMappingShader.js +26 -19
  23. package/MktMesh/mktMappingShader.ts +28 -19
  24. package/MktMesh/mktShader.d.ts +127 -58
  25. package/MktMesh/mktShader.js +192 -117
  26. package/MktMesh/mktShader.ts +251 -127
  27. package/MktMesh/modelSlot.d.ts +123 -0
  28. package/MktMesh/modelSlot.js +339 -0
  29. package/MktMesh/modelSlot.ts +383 -0
  30. package/MktMesh/panoSlotRoster.d.ts +2 -2
  31. package/MktMesh/panoSlotRoster.js +9 -7
  32. package/MktMesh/panoSlotRoster.ts +9 -7
  33. package/MktMesh/panoTileStitcher.d.ts +20 -13
  34. package/MktMesh/panoTileStitcher.js +97 -76
  35. package/MktMesh/panoTileStitcher.ts +119 -90
  36. package/PipeMesh/PipeJoint.d.ts +22 -0
  37. package/PipeMesh/PipeJoint.js +33 -0
  38. package/PipeMesh/PipeJoint.ts +38 -0
  39. package/PipeMesh/PipeMesh.d.ts +94 -22
  40. package/PipeMesh/PipeMesh.js +192 -53
  41. package/PipeMesh/PipeMesh.ts +223 -76
  42. package/PipeMesh/PipeStraight.d.ts +19 -0
  43. package/PipeMesh/PipeStraight.js +34 -0
  44. package/PipeMesh/PipeStraight.ts +45 -0
  45. package/PipeMesh/axisSnap.d.ts +52 -0
  46. package/PipeMesh/axisSnap.js +52 -0
  47. package/PipeMesh/axisSnap.ts +93 -0
  48. package/PipeMesh/directRoute.d.ts +85 -0
  49. package/PipeMesh/directRoute.js +126 -0
  50. package/PipeMesh/directRoute.ts +190 -0
  51. package/PipeMesh/index.d.ts +6 -0
  52. package/PipeMesh/index.js +6 -0
  53. package/PipeMesh/index.ts +7 -1
  54. package/PipeMesh/pipeMaterial.d.ts +16 -0
  55. package/PipeMesh/pipeMaterial.js +23 -0
  56. package/PipeMesh/pipeMaterial.ts +26 -0
  57. package/ThreeBase.d.ts +1 -39
  58. package/ThreeBase.js +38 -147
  59. package/ThreeBase.ts +1176 -1302
  60. package/instanced/InstancedBatch.d.ts +14 -1
  61. package/instanced/InstancedBatch.js +53 -3
  62. package/instanced/InstancedBatch.ts +62 -3
  63. package/instanced/InstancedChild.d.ts +24 -0
  64. package/instanced/InstancedChild.js +40 -0
  65. package/instanced/InstancedChild.ts +41 -0
  66. package/instanced/InstancedGroup.d.ts +48 -0
  67. package/instanced/InstancedGroup.js +45 -1
  68. package/instanced/InstancedGroup.ts +85 -0
  69. package/package.json +22 -7
  70. package/pass/debugDepthState.d.ts +16 -8
  71. package/pass/debugDepthState.js +4 -0
  72. package/pass/debugDepthState.ts +27 -9
  73. package/pass/glow_pass.js +11 -94
  74. package/pass/glow_pass.ts +11 -97
  75. package/pass/ssao_pass.d.ts +9 -23
  76. package/pass/ssao_pass.js +41 -128
  77. package/pass/ssao_pass.ts +36 -166
  78. package/pass/stroke_pass.js +3 -0
  79. package/pass/stroke_pass.ts +4 -0
  80. package/shaders/debugDepthCompareShader.d.ts +1 -1
  81. package/shaders/debugDepthCompareShader.js +1 -1
  82. package/shaders/debugDepthCompareShader.ts +1 -1
  83. package/shaders/debugDepthJudgeShader.d.ts +1 -1
  84. package/shaders/debugDepthJudgeShader.js +1 -1
  85. package/shaders/debugDepthJudgeShader.ts +1 -1
  86. package/PipeMesh/PathGeometry.d.ts +0 -44
  87. package/PipeMesh/PathGeometry.js +0 -307
  88. package/PipeMesh/PathGeometry.ts +0 -343
  89. package/PipeMesh/PathPoint.d.ts +0 -17
  90. package/PipeMesh/PathPoint.js +0 -31
  91. package/PipeMesh/PathPoint.ts +0 -36
  92. package/PipeMesh/PathPointList.d.ts +0 -31
  93. package/PipeMesh/PathPointList.js +0 -209
  94. package/PipeMesh/PathPointList.ts +0 -227
  95. package/PipeMesh/PathTubeGeometry.d.ts +0 -25
  96. package/PipeMesh/PathTubeGeometry.js +0 -130
  97. package/PipeMesh/PathTubeGeometry.ts +0 -146
  98. package/WaterFlow/FloatingDuck.d.ts +0 -88
  99. package/WaterFlow/FloatingDuck.js +0 -307
  100. package/WaterFlow/FloatingDuck.ts +0 -445
  101. package/WaterFlow/GPUFluidSimulation.d.ts +0 -56
  102. package/WaterFlow/GPUFluidSimulation.js +0 -785
  103. package/WaterFlow/GPUFluidSimulation.ts +0 -827
  104. package/WaterFlow/ReservoirSystem.d.ts +0 -106
  105. package/WaterFlow/ReservoirSystem.js +0 -621
  106. package/WaterFlow/ReservoirSystem.ts +0 -754
  107. package/WaterFlow/SPHField.d.ts +0 -35
  108. package/WaterFlow/SPHField.js +0 -204
  109. package/WaterFlow/SPHField.ts +0 -228
  110. package/WaterFlow/ScreenSpaceFluidPass.d.ts +0 -69
  111. package/WaterFlow/ScreenSpaceFluidPass.js +0 -736
  112. package/WaterFlow/ScreenSpaceFluidPass.ts +0 -789
  113. package/WaterFlow/ShallowWaterSurface.d.ts +0 -45
  114. package/WaterFlow/ShallowWaterSurface.js +0 -441
  115. package/WaterFlow/ShallowWaterSurface.ts +0 -510
  116. package/WaterFlow/WaterFlow.d.ts +0 -87
  117. package/WaterFlow/WaterFlow.js +0 -171
  118. package/WaterFlow/WaterFlow.ts +0 -219
  119. package/WaterFlow/types.d.ts +0 -161
  120. package/WaterFlow/types.js +0 -1
  121. package/WaterFlow/types.ts +0 -189
  122. package/WaterFlow/waterFlowPass.d.ts +0 -3
  123. package/WaterFlow/waterFlowPass.js +0 -20
  124. package/WaterFlow/waterFlowPass.ts +0 -40
  125. package/shaders/debugExcludedShader.d.ts +0 -15
  126. package/shaders/debugExcludedShader.js +0 -48
  127. package/shaders/debugExcludedShader.ts +0 -50
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 控制点轴向吸附(纯数字,零运行时依赖)。
3
+ *
4
+ * 皇上旨 2026-10-25:「添加节点和编辑节点的时候,实时计算当前节点在 x/y/z 轴上的投影距离,
5
+ * 距离谁最近就改到哪里,让管道随时保持横平竖直。」
6
+ *
7
+ * ⭐ 什么叫「投影到轴上」——不是投影到世界坐标轴,而是投影到**过参考点(相邻控制点)的三条轴线**:
8
+ * 三条候选各保留一个坐标、另两个坐标抄参考点:
9
+ * 只沿 X 走 ⇒ [p.x, ref.y, ref.z],偏离量 = √(Δy² + Δz²)
10
+ * 只沿 Y 走 ⇒ [ref.x, p.y, ref.z],偏离量 = √(Δx² + Δz²)
11
+ * 只沿 Z 走 ⇒ [ref.x, ref.y, p.z],偏离量 = √(Δx² + Δy²)
12
+ * 取偏离量最小的那条 ⇒ 该段只剩一根轴有位移 ⇒ 横平竖直。
13
+ *
14
+ * ⚠️ 三个候选都**保留待吸附点自身的那根轴坐标**(比如「只沿 X」保留 p.x)——这是关键:
15
+ * 沿 X 箭头拖动时 p.x 能自由变化,吸附不会把点钉死;被拉回的只是另外两根轴的偏移。
16
+ *
17
+ * ⚠️ 这里改的是**控制点本身**(数据层),不是走线算术:PipeMesh 仍是相邻点直连(directRoute.ts),
18
+ * 只是点的位置被挪到轴线上。⛔ 别在这里插折点 —— 那是已删除的 orthogonalRoute 老路,
19
+ * 两件事的区别是:正交走线「自动加拐点把斜线拆成三段」,这里是「把点挪正,段还是那一段」。
20
+ *
21
+ * 留成零运行时依赖(只用 `import type`,编译时擦除)⇒ `test/pipeRoute.test.js` 能直接 require
22
+ * 编译产物逐条断言,不必起浏览器。
23
+ */
24
+ import type { Vec3 } from "./directRoute";
25
+ /** 轴名(与编辑页面板的 X / Y / Z 三行同口径) */
26
+ export type AxisName = "x" | "y" | "z";
27
+ /** 吸附结果:落点 + 保留的轴 + 原位置到落点的欧氏距离 */
28
+ export interface AxisSnapResult {
29
+ /** 吸附后的位置(新数组,不改入参) */
30
+ point: Vec3;
31
+ /** 被保留(即该段唯一有位移)的轴;另两根轴已对齐到参考点 */
32
+ axis: AxisName;
33
+ /** 原位置到吸附位置的欧氏距离(米)= 这次「挪了多少」 */
34
+ distance: number;
35
+ }
36
+ /**
37
+ * 沿指定轴吸附:保留 p 在这根轴上的坐标,另外两根轴抄参考点。
38
+ * 用于面板数字输入 —— 用户输入 X 就是「沿 X 走这么远」,Y/Z 自动回落到来路,段仍是横平竖直。
39
+ */
40
+ export declare function snapAlongAxis(p: Vec3, ref: Vec3, axis: AxisName): Vec3;
41
+ /**
42
+ * 把 p 吸到「过 ref 的三条轴线」上最近的那条。
43
+ *
44
+ * ⚠️ 永远返回结果(不返回 null):p 已经在某条轴线上时,该候选的 distance 就是 0,
45
+ * 写回后坐标一个不变 —— 调用方不必再判空。
46
+ */
47
+ export declare function snapToNearestAxis(p: Vec3, ref: Vec3): AxisSnapResult;
48
+ /**
49
+ * 两点是否轴对齐(三个坐标里至少两个相等 ⇒ 这一段只沿一根轴)。
50
+ * 纯判定、不改数据:编辑页面板拿它给「这一段是不是歪的」上提示,测试拿它做断言。
51
+ */
52
+ export declare function isAxisAligned(a: Vec3, b: Vec3, eps?: number): boolean;
@@ -0,0 +1,52 @@
1
+ /** 轴名 → 分量下标 */
2
+ const AXIS_INDEX = { x: 0, y: 1, z: 2 };
3
+ /**
4
+ * 候选顺序固定 x → y → z。
5
+ * 偏离量打平时(浮点噪声级)保留先出现的那个 ⇒ 结果可复现,不会因为一次 1e-17 的抖动换轴。
6
+ */
7
+ const AXIS_ORDER = ["x", "y", "z"];
8
+ /** 判定「两轴坐标相同」的容差(与走线模块的 PIPE_EPS 同量级) */
9
+ const ALIGN_EPS = 1e-6;
10
+ /**
11
+ * 沿指定轴吸附:保留 p 在这根轴上的坐标,另外两根轴抄参考点。
12
+ * 用于面板数字输入 —— 用户输入 X 就是「沿 X 走这么远」,Y/Z 自动回落到来路,段仍是横平竖直。
13
+ */
14
+ export function snapAlongAxis(p, ref, axis) {
15
+ const k = AXIS_INDEX[axis];
16
+ const q = [ref[0], ref[1], ref[2]];
17
+ q[k] = p[k];
18
+ return q;
19
+ }
20
+ /**
21
+ * 把 p 吸到「过 ref 的三条轴线」上最近的那条。
22
+ *
23
+ * ⚠️ 永远返回结果(不返回 null):p 已经在某条轴线上时,该候选的 distance 就是 0,
24
+ * 写回后坐标一个不变 —— 调用方不必再判空。
25
+ */
26
+ export function snapToNearestAxis(p, ref) {
27
+ let best = null;
28
+ for (const axis of AXIS_ORDER) {
29
+ const point = snapAlongAxis(p, ref, axis);
30
+ const dx = point[0] - p[0];
31
+ const dy = point[1] - p[1];
32
+ const dz = point[2] - p[2];
33
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
34
+ // 用「严格小于」+ 容差:平局时保留先出现的轴,不被浮点噪声换来换去
35
+ if (!best || distance < best.distance - 1e-12)
36
+ best = { point, axis, distance };
37
+ }
38
+ // 循环一定跑过一轮 ⇒ best 非空;写这个兜底只为满足 strict 关闭时的可读性
39
+ return best;
40
+ }
41
+ /**
42
+ * 两点是否轴对齐(三个坐标里至少两个相等 ⇒ 这一段只沿一根轴)。
43
+ * 纯判定、不改数据:编辑页面板拿它给「这一段是不是歪的」上提示,测试拿它做断言。
44
+ */
45
+ export function isAxisAligned(a, b, eps = ALIGN_EPS) {
46
+ let same = 0;
47
+ for (let i = 0; i < 3; i++) {
48
+ if (Math.abs(a[i] - b[i]) < eps)
49
+ same++;
50
+ }
51
+ return same >= 2;
52
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 控制点轴向吸附(纯数字,零运行时依赖)。
3
+ *
4
+ * 皇上旨 2026-10-25:「添加节点和编辑节点的时候,实时计算当前节点在 x/y/z 轴上的投影距离,
5
+ * 距离谁最近就改到哪里,让管道随时保持横平竖直。」
6
+ *
7
+ * ⭐ 什么叫「投影到轴上」——不是投影到世界坐标轴,而是投影到**过参考点(相邻控制点)的三条轴线**:
8
+ * 三条候选各保留一个坐标、另两个坐标抄参考点:
9
+ * 只沿 X 走 ⇒ [p.x, ref.y, ref.z],偏离量 = √(Δy² + Δz²)
10
+ * 只沿 Y 走 ⇒ [ref.x, p.y, ref.z],偏离量 = √(Δx² + Δz²)
11
+ * 只沿 Z 走 ⇒ [ref.x, ref.y, p.z],偏离量 = √(Δx² + Δy²)
12
+ * 取偏离量最小的那条 ⇒ 该段只剩一根轴有位移 ⇒ 横平竖直。
13
+ *
14
+ * ⚠️ 三个候选都**保留待吸附点自身的那根轴坐标**(比如「只沿 X」保留 p.x)——这是关键:
15
+ * 沿 X 箭头拖动时 p.x 能自由变化,吸附不会把点钉死;被拉回的只是另外两根轴的偏移。
16
+ *
17
+ * ⚠️ 这里改的是**控制点本身**(数据层),不是走线算术:PipeMesh 仍是相邻点直连(directRoute.ts),
18
+ * 只是点的位置被挪到轴线上。⛔ 别在这里插折点 —— 那是已删除的 orthogonalRoute 老路,
19
+ * 两件事的区别是:正交走线「自动加拐点把斜线拆成三段」,这里是「把点挪正,段还是那一段」。
20
+ *
21
+ * 留成零运行时依赖(只用 `import type`,编译时擦除)⇒ `test/pipeRoute.test.js` 能直接 require
22
+ * 编译产物逐条断言,不必起浏览器。
23
+ */
24
+ import type { Vec3 } from "./directRoute";
25
+
26
+ /** 轴名(与编辑页面板的 X / Y / Z 三行同口径) */
27
+ export type AxisName = "x" | "y" | "z";
28
+
29
+ /** 吸附结果:落点 + 保留的轴 + 原位置到落点的欧氏距离 */
30
+ export interface AxisSnapResult {
31
+ /** 吸附后的位置(新数组,不改入参) */
32
+ point: Vec3;
33
+ /** 被保留(即该段唯一有位移)的轴;另两根轴已对齐到参考点 */
34
+ axis: AxisName;
35
+ /** 原位置到吸附位置的欧氏距离(米)= 这次「挪了多少」 */
36
+ distance: number;
37
+ }
38
+
39
+ /** 轴名 → 分量下标 */
40
+ const AXIS_INDEX: Record<AxisName, 0 | 1 | 2> = { x: 0, y: 1, z: 2 };
41
+
42
+ /**
43
+ * 候选顺序固定 x → y → z。
44
+ * 偏离量打平时(浮点噪声级)保留先出现的那个 ⇒ 结果可复现,不会因为一次 1e-17 的抖动换轴。
45
+ */
46
+ const AXIS_ORDER: AxisName[] = ["x", "y", "z"];
47
+
48
+ /** 判定「两轴坐标相同」的容差(与走线模块的 PIPE_EPS 同量级) */
49
+ const ALIGN_EPS = 1e-6;
50
+
51
+ /**
52
+ * 沿指定轴吸附:保留 p 在这根轴上的坐标,另外两根轴抄参考点。
53
+ * 用于面板数字输入 —— 用户输入 X 就是「沿 X 走这么远」,Y/Z 自动回落到来路,段仍是横平竖直。
54
+ */
55
+ export function snapAlongAxis(p: Vec3, ref: Vec3, axis: AxisName): Vec3 {
56
+ const k = AXIS_INDEX[axis];
57
+ const q: Vec3 = [ref[0], ref[1], ref[2]];
58
+ q[k] = p[k];
59
+ return q;
60
+ }
61
+
62
+ /**
63
+ * 把 p 吸到「过 ref 的三条轴线」上最近的那条。
64
+ *
65
+ * ⚠️ 永远返回结果(不返回 null):p 已经在某条轴线上时,该候选的 distance 就是 0,
66
+ * 写回后坐标一个不变 —— 调用方不必再判空。
67
+ */
68
+ export function snapToNearestAxis(p: Vec3, ref: Vec3): AxisSnapResult {
69
+ let best: AxisSnapResult | null = null;
70
+ for (const axis of AXIS_ORDER) {
71
+ const point = snapAlongAxis(p, ref, axis);
72
+ const dx = point[0] - p[0];
73
+ const dy = point[1] - p[1];
74
+ const dz = point[2] - p[2];
75
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
76
+ // 用「严格小于」+ 容差:平局时保留先出现的轴,不被浮点噪声换来换去
77
+ if (!best || distance < best.distance - 1e-12) best = { point, axis, distance };
78
+ }
79
+ // 循环一定跑过一轮 ⇒ best 非空;写这个兜底只为满足 strict 关闭时的可读性
80
+ return best as AxisSnapResult;
81
+ }
82
+
83
+ /**
84
+ * 两点是否轴对齐(三个坐标里至少两个相等 ⇒ 这一段只沿一根轴)。
85
+ * 纯判定、不改数据:编辑页面板拿它给「这一段是不是歪的」上提示,测试拿它做断言。
86
+ */
87
+ export function isAxisAligned(a: Vec3, b: Vec3, eps = ALIGN_EPS): boolean {
88
+ let same = 0;
89
+ for (let i = 0; i < 3; i++) {
90
+ if (Math.abs(a[i] - b[i]) < eps) same++;
91
+ }
92
+ return same >= 2;
93
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * 直连管道走线(纯数字,零依赖)。
3
+ *
4
+ * 皇上旨 2026-10-25「直接就是点与点直接连接,最简单的方案」:
5
+ * 相邻两个控制点之间就是一根直管(可以是斜的),不再按轴序拆正交段、不再有 90° 弯头。
6
+ * 每段只算三件事:长度 = 两点欧氏距离、方向 = 连线单位向量、中点 = 两点中点。
7
+ *
8
+ * ⭐ 为什么仍单独一个零 import 模块:管道几何要写进 InstancedChild 的实例矩阵(见 PipeMesh.ts),
9
+ * 而「几段、每段多长、朝哪、球放哪」是可独立核对的算术。留成零依赖 ⇒ `test/pipeRoute.test.js`
10
+ * 能直接 require 编译产物逐字段断言,不必起浏览器。
11
+ *
12
+ * ⚠️ 拐角为什么必须补一颗球:直管是**开口圆柱**(CylinderGeometry openEnded,见 PipeStraight.ts),
13
+ * 两段斜管在拐角处只有轴线相交、端面是斜的 ⇒ 夹角外侧必然露一个楔形缺口,能直接看穿管子。
14
+ * 补法取最省事的一种:控制点上放一颗「直径同管」的球(PipeJoint),球面把缺口盖满。
15
+ * 球与管同径 ⇒ 共线时完全看不出来,有夹角时是光滑过渡,不是一串珠子。
16
+ *
17
+ * ⚠️ 上一版(`orthogonalRoute.ts`,2026-10-21 ~ 10-25)是按 axisOrder 逐轴补齐的正交走线 + 90° 弯头,
18
+ * 已整份删除。要找回请从 git 取,别在半路上重新发明一遍。
19
+ */
20
+ /** 世界坐标点,单位米 */
21
+ export type Vec3 = [number, number, number];
22
+ /** 直管段:几何中心 + 轴向单位向量 + 段长 */
23
+ export interface StraightPart {
24
+ /** 段序号(从 0 起,对应「控制点 i → 控制点 i+1」) */
25
+ index: number;
26
+ /** 直管几何体的中心(单位圆柱沿 +Y 生成,容器再旋转/缩放到此位姿) */
27
+ center: Vec3;
28
+ /** 走线方向,单位向量(任意朝向,不限六向) */
29
+ dir: Vec3;
30
+ /** 段长(米)= 两控制点欧氏距离 */
31
+ length: number;
32
+ }
33
+ /** 拐角补角球:球心压在控制点上,半径 = 直径/2 */
34
+ export interface JointPart {
35
+ /** 对应控制点下标(1 … n-2;两端没有拐角,不补球) */
36
+ index: number;
37
+ center: Vec3;
38
+ radius: number;
39
+ }
40
+ export interface PipePlan {
41
+ straights: StraightPart[];
42
+ joints: JointPart[];
43
+ /** 退化输入说明(点重合、走线全零、直径非法…);渲染不受影响,供面板提示 */
44
+ warnings: string[];
45
+ }
46
+ /** 段长可忽略的判据(与上一版正交方案同量级) */
47
+ export declare const PIPE_EPS = 0.000001;
48
+ /**
49
+ * 算出整条管道的直管 + 补角球清单。
50
+ *
51
+ * ⚠️ 重合的控制点对**不产生直管**(长度 0 的圆柱缩放会让法线退化),但两端点仍参与球的判定 ——
52
+ * 只要该点还连着别的有效直管,球照放,接缝才不会因为一个废段而开口。
53
+ */
54
+ export declare function planDirectLayout(points: Vec3[], opts?: {
55
+ diameter?: number;
56
+ }): PipePlan;
57
+ /**
58
+ * JSON 是「管道对象」与「编辑页项目」共用的唯一数据形状。
59
+ * 单位米;points 是控制点(不是段),所以重新走线/改直径只改这几个字段即可。
60
+ *
61
+ * ⚠️ version 仍是 1:形状(points / diameter / color)与上一版一致,只是上一版的 `axisOrder`
62
+ * 从「必填参数」降级成「忽略的多余键」⇒ 皇上库里的旧项目照样打得开,只是形状按直连重算。
63
+ */
64
+ export interface PipeJson {
65
+ format: "three-base.pipe";
66
+ version: 1;
67
+ diameter: number;
68
+ color?: string;
69
+ points: Vec3[];
70
+ }
71
+ export declare const PIPE_JSON_FORMAT = "three-base.pipe";
72
+ export declare const PIPE_JSON_VERSION = 1;
73
+ export declare function makePipeJson(points: Vec3[], diameter: number, color?: string): PipeJson;
74
+ /**
75
+ * 解析结果:`ok=false` 时 `reason` 是人话,可直接弹给编辑页显示;`ok=true` 时 `json` 已归一化。
76
+ * ⚠️ 刻意不用 `{ok:true;json} | {ok:false;reason}` 可辨识联合:库编译走的是 `strict:false`
77
+ * (three-base/tsconfig.json),实测那种写法在 `if (!r.ok)` 之后取 `r.reason` 会报 TS2339。
78
+ */
79
+ export interface PipeJsonResult {
80
+ ok: boolean;
81
+ json?: PipeJson;
82
+ reason?: string;
83
+ }
84
+ /** 宽松校验:接受对象/字符串,字段缺失或类型错都返回 reason 而不抛异常(导入现场不许崩) */
85
+ export declare function parsePipeJson(input: unknown): PipeJsonResult;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * 直连管道走线(纯数字,零依赖)。
3
+ *
4
+ * 皇上旨 2026-10-25「直接就是点与点直接连接,最简单的方案」:
5
+ * 相邻两个控制点之间就是一根直管(可以是斜的),不再按轴序拆正交段、不再有 90° 弯头。
6
+ * 每段只算三件事:长度 = 两点欧氏距离、方向 = 连线单位向量、中点 = 两点中点。
7
+ *
8
+ * ⭐ 为什么仍单独一个零 import 模块:管道几何要写进 InstancedChild 的实例矩阵(见 PipeMesh.ts),
9
+ * 而「几段、每段多长、朝哪、球放哪」是可独立核对的算术。留成零依赖 ⇒ `test/pipeRoute.test.js`
10
+ * 能直接 require 编译产物逐字段断言,不必起浏览器。
11
+ *
12
+ * ⚠️ 拐角为什么必须补一颗球:直管是**开口圆柱**(CylinderGeometry openEnded,见 PipeStraight.ts),
13
+ * 两段斜管在拐角处只有轴线相交、端面是斜的 ⇒ 夹角外侧必然露一个楔形缺口,能直接看穿管子。
14
+ * 补法取最省事的一种:控制点上放一颗「直径同管」的球(PipeJoint),球面把缺口盖满。
15
+ * 球与管同径 ⇒ 共线时完全看不出来,有夹角时是光滑过渡,不是一串珠子。
16
+ *
17
+ * ⚠️ 上一版(`orthogonalRoute.ts`,2026-10-21 ~ 10-25)是按 axisOrder 逐轴补齐的正交走线 + 90° 弯头,
18
+ * 已整份删除。要找回请从 git 取,别在半路上重新发明一遍。
19
+ */
20
+ /** 段长可忽略的判据(与上一版正交方案同量级) */
21
+ export const PIPE_EPS = 1e-6;
22
+ /**
23
+ * 算出整条管道的直管 + 补角球清单。
24
+ *
25
+ * ⚠️ 重合的控制点对**不产生直管**(长度 0 的圆柱缩放会让法线退化),但两端点仍参与球的判定 ——
26
+ * 只要该点还连着别的有效直管,球照放,接缝才不会因为一个废段而开口。
27
+ */
28
+ export function planDirectLayout(points, opts = {}) {
29
+ const diameter = opts.diameter ?? 1;
30
+ const warnings = [];
31
+ const radius = diameter / 2;
32
+ if (points.length < 2) {
33
+ warnings.push("控制点少于 2 个,无法走线");
34
+ return { straights: [], joints: [], warnings };
35
+ }
36
+ if (!(diameter > 0)) {
37
+ warnings.push(`直径 ${diameter} 非法(必须为正数,单位米)`);
38
+ return { straights: [], joints: [], warnings };
39
+ }
40
+ const straights = [];
41
+ /** 第 i 段(控制点 i → i+1)是否真的产生了一根直管 */
42
+ const segOk = [];
43
+ for (let i = 0; i < points.length - 1; i++) {
44
+ const a = points[i];
45
+ const b = points[i + 1];
46
+ const dx = b[0] - a[0];
47
+ const dy = b[1] - a[1];
48
+ const dz = b[2] - a[2];
49
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
50
+ if (len < PIPE_EPS) {
51
+ segOk.push(false);
52
+ warnings.push(`第 ${i + 1} 与第 ${i + 2} 个控制点重合,跳过这一段`);
53
+ continue;
54
+ }
55
+ segOk.push(true);
56
+ straights.push({
57
+ index: i,
58
+ center: [a[0] + dx / 2, a[1] + dy / 2, a[2] + dz / 2],
59
+ dir: [dx / len, dy / len, dz / len],
60
+ length: len,
61
+ });
62
+ }
63
+ const joints = [];
64
+ for (let i = 1; i < points.length - 1; i++) {
65
+ // 前后都没生出直管 ⇒ 这是个孤立点,补球只是凭空多一颗,没意义
66
+ if (!segOk[i - 1] && !segOk[i])
67
+ continue;
68
+ const p = points[i];
69
+ joints.push({ index: i, center: [p[0], p[1], p[2]], radius });
70
+ }
71
+ if (straights.length === 0 && warnings.length === 0) {
72
+ warnings.push("没有产生任何直管");
73
+ }
74
+ return { straights, joints, warnings };
75
+ }
76
+ export const PIPE_JSON_FORMAT = "three-base.pipe";
77
+ export const PIPE_JSON_VERSION = 1;
78
+ export function makePipeJson(points, diameter, color) {
79
+ const json = { format: PIPE_JSON_FORMAT, version: PIPE_JSON_VERSION, diameter, points };
80
+ if (color)
81
+ json.color = color;
82
+ return json;
83
+ }
84
+ /** 宽松校验:接受对象/字符串,字段缺失或类型错都返回 reason 而不抛异常(导入现场不许崩) */
85
+ export function parsePipeJson(input) {
86
+ let raw = input;
87
+ if (typeof raw === "string") {
88
+ try {
89
+ raw = JSON.parse(raw);
90
+ }
91
+ catch (e) {
92
+ return { ok: false, reason: "不是合法 JSON 文本" };
93
+ }
94
+ }
95
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
96
+ return { ok: false, reason: "顶层不是 JSON 对象" };
97
+ }
98
+ const obj = raw;
99
+ if (obj.format !== PIPE_JSON_FORMAT) {
100
+ return { ok: false, reason: `format 应为 "${PIPE_JSON_FORMAT}",实际 ${JSON.stringify(obj.format)}` };
101
+ }
102
+ if (obj.version !== PIPE_JSON_VERSION) {
103
+ return { ok: false, reason: `暂不支持 version ${String(obj.version)}(当前只认 ${PIPE_JSON_VERSION})` };
104
+ }
105
+ const points = obj.points;
106
+ if (!Array.isArray(points) || points.length < 2) {
107
+ return { ok: false, reason: "points 至少要有 2 个控制点" };
108
+ }
109
+ const vecs = [];
110
+ for (let i = 0; i < points.length; i++) {
111
+ const p = points[i];
112
+ const isTuple = Array.isArray(p) && p.length === 3 && p.every((n) => typeof n === "number" && Number.isFinite(n));
113
+ if (!isTuple) {
114
+ return { ok: false, reason: `第 ${i + 1} 个控制点不是 [x,y,z] 三个有限数字` };
115
+ }
116
+ vecs.push([p[0], p[1], p[2]]);
117
+ }
118
+ const diameter = typeof obj.diameter === "number" && Number.isFinite(obj.diameter) && obj.diameter > 0
119
+ ? obj.diameter
120
+ : NaN;
121
+ if (!(diameter > 0)) {
122
+ return { ok: false, reason: "diameter 缺失或不是正数(单位米)" };
123
+ }
124
+ const color = typeof obj.color === "string" ? obj.color : undefined;
125
+ return { ok: true, json: makePipeJson(vecs, diameter, color) };
126
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * 直连管道走线(纯数字,零依赖)。
3
+ *
4
+ * 皇上旨 2026-10-25「直接就是点与点直接连接,最简单的方案」:
5
+ * 相邻两个控制点之间就是一根直管(可以是斜的),不再按轴序拆正交段、不再有 90° 弯头。
6
+ * 每段只算三件事:长度 = 两点欧氏距离、方向 = 连线单位向量、中点 = 两点中点。
7
+ *
8
+ * ⭐ 为什么仍单独一个零 import 模块:管道几何要写进 InstancedChild 的实例矩阵(见 PipeMesh.ts),
9
+ * 而「几段、每段多长、朝哪、球放哪」是可独立核对的算术。留成零依赖 ⇒ `test/pipeRoute.test.js`
10
+ * 能直接 require 编译产物逐字段断言,不必起浏览器。
11
+ *
12
+ * ⚠️ 拐角为什么必须补一颗球:直管是**开口圆柱**(CylinderGeometry openEnded,见 PipeStraight.ts),
13
+ * 两段斜管在拐角处只有轴线相交、端面是斜的 ⇒ 夹角外侧必然露一个楔形缺口,能直接看穿管子。
14
+ * 补法取最省事的一种:控制点上放一颗「直径同管」的球(PipeJoint),球面把缺口盖满。
15
+ * 球与管同径 ⇒ 共线时完全看不出来,有夹角时是光滑过渡,不是一串珠子。
16
+ *
17
+ * ⚠️ 上一版(`orthogonalRoute.ts`,2026-10-21 ~ 10-25)是按 axisOrder 逐轴补齐的正交走线 + 90° 弯头,
18
+ * 已整份删除。要找回请从 git 取,别在半路上重新发明一遍。
19
+ */
20
+
21
+ /** 世界坐标点,单位米 */
22
+ export type Vec3 = [number, number, number];
23
+
24
+ /** 直管段:几何中心 + 轴向单位向量 + 段长 */
25
+ export interface StraightPart {
26
+ /** 段序号(从 0 起,对应「控制点 i → 控制点 i+1」) */
27
+ index: number;
28
+ /** 直管几何体的中心(单位圆柱沿 +Y 生成,容器再旋转/缩放到此位姿) */
29
+ center: Vec3;
30
+ /** 走线方向,单位向量(任意朝向,不限六向) */
31
+ dir: Vec3;
32
+ /** 段长(米)= 两控制点欧氏距离 */
33
+ length: number;
34
+ }
35
+
36
+ /** 拐角补角球:球心压在控制点上,半径 = 直径/2 */
37
+ export interface JointPart {
38
+ /** 对应控制点下标(1 … n-2;两端没有拐角,不补球) */
39
+ index: number;
40
+ center: Vec3;
41
+ radius: number;
42
+ }
43
+
44
+ export interface PipePlan {
45
+ straights: StraightPart[];
46
+ joints: JointPart[];
47
+ /** 退化输入说明(点重合、走线全零、直径非法…);渲染不受影响,供面板提示 */
48
+ warnings: string[];
49
+ }
50
+
51
+ /** 段长可忽略的判据(与上一版正交方案同量级) */
52
+ export const PIPE_EPS = 1e-6;
53
+
54
+ /**
55
+ * 算出整条管道的直管 + 补角球清单。
56
+ *
57
+ * ⚠️ 重合的控制点对**不产生直管**(长度 0 的圆柱缩放会让法线退化),但两端点仍参与球的判定 ——
58
+ * 只要该点还连着别的有效直管,球照放,接缝才不会因为一个废段而开口。
59
+ */
60
+ export function planDirectLayout(points: Vec3[], opts: { diameter?: number } = {}): PipePlan {
61
+ const diameter = opts.diameter ?? 1;
62
+ const warnings: string[] = [];
63
+ const radius = diameter / 2;
64
+
65
+ if (points.length < 2) {
66
+ warnings.push("控制点少于 2 个,无法走线");
67
+ return { straights: [], joints: [], warnings };
68
+ }
69
+ if (!(diameter > 0)) {
70
+ warnings.push(`直径 ${diameter} 非法(必须为正数,单位米)`);
71
+ return { straights: [], joints: [], warnings };
72
+ }
73
+
74
+ const straights: StraightPart[] = [];
75
+ /** 第 i 段(控制点 i → i+1)是否真的产生了一根直管 */
76
+ const segOk: boolean[] = [];
77
+
78
+ for (let i = 0; i < points.length - 1; i++) {
79
+ const a = points[i];
80
+ const b = points[i + 1];
81
+ const dx = b[0] - a[0];
82
+ const dy = b[1] - a[1];
83
+ const dz = b[2] - a[2];
84
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
85
+ if (len < PIPE_EPS) {
86
+ segOk.push(false);
87
+ warnings.push(`第 ${i + 1} 与第 ${i + 2} 个控制点重合,跳过这一段`);
88
+ continue;
89
+ }
90
+ segOk.push(true);
91
+ straights.push({
92
+ index: i,
93
+ center: [a[0] + dx / 2, a[1] + dy / 2, a[2] + dz / 2],
94
+ dir: [dx / len, dy / len, dz / len],
95
+ length: len,
96
+ });
97
+ }
98
+
99
+ const joints: JointPart[] = [];
100
+ for (let i = 1; i < points.length - 1; i++) {
101
+ // 前后都没生出直管 ⇒ 这是个孤立点,补球只是凭空多一颗,没意义
102
+ if (!segOk[i - 1] && !segOk[i]) continue;
103
+ const p = points[i];
104
+ joints.push({ index: i, center: [p[0], p[1], p[2]], radius });
105
+ }
106
+
107
+ if (straights.length === 0 && warnings.length === 0) {
108
+ warnings.push("没有产生任何直管");
109
+ }
110
+ return { straights, joints, warnings };
111
+ }
112
+
113
+ /**
114
+ * JSON 是「管道对象」与「编辑页项目」共用的唯一数据形状。
115
+ * 单位米;points 是控制点(不是段),所以重新走线/改直径只改这几个字段即可。
116
+ *
117
+ * ⚠️ version 仍是 1:形状(points / diameter / color)与上一版一致,只是上一版的 `axisOrder`
118
+ * 从「必填参数」降级成「忽略的多余键」⇒ 皇上库里的旧项目照样打得开,只是形状按直连重算。
119
+ */
120
+ export interface PipeJson {
121
+ format: "three-base.pipe";
122
+ version: 1;
123
+ diameter: number;
124
+ color?: string;
125
+ points: Vec3[];
126
+ }
127
+
128
+ export const PIPE_JSON_FORMAT = "three-base.pipe";
129
+ export const PIPE_JSON_VERSION = 1;
130
+
131
+ export function makePipeJson(points: Vec3[], diameter: number, color?: string): PipeJson {
132
+ const json: PipeJson = { format: PIPE_JSON_FORMAT, version: PIPE_JSON_VERSION, diameter, points };
133
+ if (color) json.color = color;
134
+ return json;
135
+ }
136
+
137
+ /**
138
+ * 解析结果:`ok=false` 时 `reason` 是人话,可直接弹给编辑页显示;`ok=true` 时 `json` 已归一化。
139
+ * ⚠️ 刻意不用 `{ok:true;json} | {ok:false;reason}` 可辨识联合:库编译走的是 `strict:false`
140
+ * (three-base/tsconfig.json),实测那种写法在 `if (!r.ok)` 之后取 `r.reason` 会报 TS2339。
141
+ */
142
+ export interface PipeJsonResult {
143
+ ok: boolean;
144
+ json?: PipeJson;
145
+ reason?: string;
146
+ }
147
+
148
+ /** 宽松校验:接受对象/字符串,字段缺失或类型错都返回 reason 而不抛异常(导入现场不许崩) */
149
+ export function parsePipeJson(input: unknown): PipeJsonResult {
150
+ let raw: unknown = input;
151
+ if (typeof raw === "string") {
152
+ try {
153
+ raw = JSON.parse(raw);
154
+ } catch (e) {
155
+ return { ok: false, reason: "不是合法 JSON 文本" };
156
+ }
157
+ }
158
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
159
+ return { ok: false, reason: "顶层不是 JSON 对象" };
160
+ }
161
+ const obj = raw as Record<string, unknown>;
162
+ if (obj.format !== PIPE_JSON_FORMAT) {
163
+ return { ok: false, reason: `format 应为 "${PIPE_JSON_FORMAT}",实际 ${JSON.stringify(obj.format)}` };
164
+ }
165
+ if (obj.version !== PIPE_JSON_VERSION) {
166
+ return { ok: false, reason: `暂不支持 version ${String(obj.version)}(当前只认 ${PIPE_JSON_VERSION})` };
167
+ }
168
+ const points = obj.points;
169
+ if (!Array.isArray(points) || points.length < 2) {
170
+ return { ok: false, reason: "points 至少要有 2 个控制点" };
171
+ }
172
+ const vecs: Vec3[] = [];
173
+ for (let i = 0; i < points.length; i++) {
174
+ const p = points[i];
175
+ const isTuple =
176
+ Array.isArray(p) && p.length === 3 && p.every((n) => typeof n === "number" && Number.isFinite(n));
177
+ if (!isTuple) {
178
+ return { ok: false, reason: `第 ${i + 1} 个控制点不是 [x,y,z] 三个有限数字` };
179
+ }
180
+ vecs.push([p[0] as number, p[1] as number, p[2] as number]);
181
+ }
182
+ const diameter = typeof obj.diameter === "number" && Number.isFinite(obj.diameter) && obj.diameter > 0
183
+ ? obj.diameter
184
+ : NaN;
185
+ if (!(diameter > 0)) {
186
+ return { ok: false, reason: "diameter 缺失或不是正数(单位米)" };
187
+ }
188
+ const color = typeof obj.color === "string" ? obj.color : undefined;
189
+ return { ok: true, json: makePipeJson(vecs, diameter, color) };
190
+ }
@@ -1 +1,7 @@
1
1
  export { default } from "./PipeMesh";
2
+ export { default as PipeMesh } from "./PipeMesh";
3
+ export { default as PipeStraight } from "./PipeStraight";
4
+ export { default as PipeJoint } from "./PipeJoint";
5
+ export { createPipeMaterial, pipeMaterial, PIPE_DEFAULT_COLOR } from "./pipeMaterial";
6
+ export * from "./directRoute";
7
+ export * from "./axisSnap";
package/PipeMesh/index.js CHANGED
@@ -1 +1,7 @@
1
1
  export { default } from "./PipeMesh";
2
+ export { default as PipeMesh } from "./PipeMesh";
3
+ export { default as PipeStraight } from "./PipeStraight";
4
+ export { default as PipeJoint } from "./PipeJoint";
5
+ export { createPipeMaterial, pipeMaterial, PIPE_DEFAULT_COLOR } from "./pipeMaterial";
6
+ export * from "./directRoute";
7
+ export * from "./axisSnap";
package/PipeMesh/index.ts CHANGED
@@ -1 +1,7 @@
1
- export { default } from "./PipeMesh";
1
+ export { default } from "./PipeMesh";
2
+ export { default as PipeMesh } from "./PipeMesh";
3
+ export { default as PipeStraight } from "./PipeStraight";
4
+ export { default as PipeJoint } from "./PipeJoint";
5
+ export { createPipeMaterial, pipeMaterial, PIPE_DEFAULT_COLOR } from "./pipeMaterial";
6
+ export * from "./directRoute";
7
+ export * from "./axisSnap";
@@ -0,0 +1,16 @@
1
+ import { MeshStandardMaterial } from "three";
2
+ /**
3
+ * 直管与弯头共用一份材质。
4
+ *
5
+ * ⭐ 为什么底色是白:实例化那条路,逐实例颜色走 `InstancedMesh.instanceColor`(InstancedBatch 预填 1.0,
6
+ * setColor 覆盖),three 里最终 diffuse = material.color × instanceColor。材质底色若非白,setColor 的值
7
+ * 就会被整体压暗,面板上「这根管子涂什么色」就对不上画面了。
8
+ * ⭐ 为什么两边共享同一份:材质只有一份 ⇒ 粗糙度/金属度改一次,两个 batch(直管、弯头)同时跟上;
9
+ * 合批是按 geometry+material 分桶的,共享材质不会把它们并成一个 batch(几何不同),draw call 仍是 2。
10
+ * ⚠️ DoubleSide 是必需的:直管是开口圆柱(无端面),从管口往里看要能看到内壁,否则管子里是黑的空洞。
11
+ */
12
+ export declare function createPipeMaterial(): MeshStandardMaterial;
13
+ /** 全库共享的那一份材质实例(InstancedChild 的 static material 必须是同一个对象才会同桶/同改) */
14
+ export declare const pipeMaterial: MeshStandardMaterial;
15
+ /** 管道默认颜色,取自参考稿 */
16
+ export declare const PIPE_DEFAULT_COLOR = 5227511;
@@ -0,0 +1,23 @@
1
+ import { DoubleSide, MeshStandardMaterial } from "three";
2
+ /**
3
+ * 直管与弯头共用一份材质。
4
+ *
5
+ * ⭐ 为什么底色是白:实例化那条路,逐实例颜色走 `InstancedMesh.instanceColor`(InstancedBatch 预填 1.0,
6
+ * setColor 覆盖),three 里最终 diffuse = material.color × instanceColor。材质底色若非白,setColor 的值
7
+ * 就会被整体压暗,面板上「这根管子涂什么色」就对不上画面了。
8
+ * ⭐ 为什么两边共享同一份:材质只有一份 ⇒ 粗糙度/金属度改一次,两个 batch(直管、弯头)同时跟上;
9
+ * 合批是按 geometry+material 分桶的,共享材质不会把它们并成一个 batch(几何不同),draw call 仍是 2。
10
+ * ⚠️ DoubleSide 是必需的:直管是开口圆柱(无端面),从管口往里看要能看到内壁,否则管子里是黑的空洞。
11
+ */
12
+ export function createPipeMaterial() {
13
+ return new MeshStandardMaterial({
14
+ color: 0xffffff,
15
+ roughness: 0.35,
16
+ metalness: 0.5,
17
+ side: DoubleSide,
18
+ });
19
+ }
20
+ /** 全库共享的那一份材质实例(InstancedChild 的 static material 必须是同一个对象才会同桶/同改) */
21
+ export const pipeMaterial = createPipeMaterial();
22
+ /** 管道默认颜色,取自参考稿 */
23
+ export const PIPE_DEFAULT_COLOR = 0x4fc3f7;