@arronqzy/view-scene3d 0.1.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/package.json +72 -0
- package/src/animation.ts +26 -0
- package/src/defaults.ts +149 -0
- package/src/index.ts +6 -0
- package/src/model-names.ts +21 -0
- package/src/object-state.ts +28 -0
- package/src/pivot.ts +29 -0
- package/src/react/Scene3dA11y.tsx +23 -0
- package/src/react/Scene3dEffects.tsx +14 -0
- package/src/react/Scene3dNodeContent.tsx +1116 -0
- package/src/react/Scene3dPhysics.tsx +15 -0
- package/src/react/ecosystem.ts +28 -0
- package/src/react/index.ts +8 -0
- package/src/types.ts +124 -0
- package/src/vue/Scene3dNodeContent.vue +494 -0
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arronqzy/view-scene3d",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Shared 3D scene types and React/Vue renderers for Abuilder (R3F + Three.js)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"src"
|
|
9
|
+
],
|
|
10
|
+
"main": "./src/index.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.ts",
|
|
13
|
+
"./react": "./src/react/index.ts",
|
|
14
|
+
"./react/ecosystem": "./src/react/ecosystem.ts",
|
|
15
|
+
"./vue": "./src/vue/Scene3dNodeContent.vue"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@react-spring/three": "^9.7.5",
|
|
19
|
+
"@react-three/a11y": "^3.0.0",
|
|
20
|
+
"@react-three/cannon": "^6.6.0",
|
|
21
|
+
"@react-three/csg": "^3.0.0",
|
|
22
|
+
"@react-three/drei": "^9.122.0",
|
|
23
|
+
"@react-three/fiber": "^8.17.14",
|
|
24
|
+
"@react-three/flex": "^1.0.1",
|
|
25
|
+
"@react-three/gpu-pathtracer": "^0.3.2",
|
|
26
|
+
"@react-three/p2": "^0.0.6",
|
|
27
|
+
"@react-three/postprocessing": "^2.19.1",
|
|
28
|
+
"@react-three/rapier": "^1.4.0",
|
|
29
|
+
"@react-three/xr": "^5.7.1",
|
|
30
|
+
"@use-gesture/react": "^10.3.1",
|
|
31
|
+
"lamina": "^1.1.1",
|
|
32
|
+
"leva": "^0.9.35",
|
|
33
|
+
"maath": "^0.10.8",
|
|
34
|
+
"miniplex": "^2.0.0",
|
|
35
|
+
"postprocessing": "^6.36.0",
|
|
36
|
+
"three": "^0.172.0",
|
|
37
|
+
"three-stdlib": "^2.35.0",
|
|
38
|
+
"valtio": "^2.3.2",
|
|
39
|
+
"@arronqzy/i18n": "0.1.7"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": ">=18",
|
|
43
|
+
"react-dom": ">=18",
|
|
44
|
+
"vue": "^3.4.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"react": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"react-dom": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"vue": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@react-three/gltfjsx": "^4.3.4",
|
|
59
|
+
"@react-three/test-renderer": "^8.2.1",
|
|
60
|
+
"@types/react": "^19.0.1",
|
|
61
|
+
"@types/three": "^0.172.0",
|
|
62
|
+
"typescript": "^5.5.4",
|
|
63
|
+
"@arronqzy/typescript-config": "1.0.0"
|
|
64
|
+
},
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
|
70
|
+
"build": "pnpm run typecheck"
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/animation.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Scene3dCameraState } from "./types";
|
|
2
|
+
|
|
3
|
+
export function lerpCamera(
|
|
4
|
+
from: Scene3dCameraState,
|
|
5
|
+
to: Scene3dCameraState,
|
|
6
|
+
t: number
|
|
7
|
+
): Scene3dCameraState {
|
|
8
|
+
const clamp = Math.min(1, Math.max(0, t));
|
|
9
|
+
const lerp3 = (a: [number, number, number], b: [number, number, number]) =>
|
|
10
|
+
[
|
|
11
|
+
a[0] + (b[0] - a[0]) * clamp,
|
|
12
|
+
a[1] + (b[1] - a[1]) * clamp,
|
|
13
|
+
a[2] + (b[2] - a[2]) * clamp,
|
|
14
|
+
] as [number, number, number];
|
|
15
|
+
return {
|
|
16
|
+
position: lerp3(from.position, to.position),
|
|
17
|
+
target: lerp3(from.target, to.target),
|
|
18
|
+
fov: from.fov + (to.fov - from.fov) * clamp,
|
|
19
|
+
near: from.near,
|
|
20
|
+
far: from.far,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function easeInOutCubic(t: number): number {
|
|
25
|
+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
26
|
+
}
|
package/src/defaults.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Scene3dConfig,
|
|
3
|
+
Scene3dPresentation,
|
|
4
|
+
Scene3dPurpose,
|
|
5
|
+
} from "./types";
|
|
6
|
+
|
|
7
|
+
export function createDefaultPresentation(
|
|
8
|
+
purpose: Scene3dPurpose = "product"
|
|
9
|
+
): Scene3dPresentation {
|
|
10
|
+
if (purpose === "twin") {
|
|
11
|
+
return {
|
|
12
|
+
purpose,
|
|
13
|
+
environmentPreset: "warehouse",
|
|
14
|
+
showContactShadows: true,
|
|
15
|
+
showGrid: true,
|
|
16
|
+
clickToFocus: true,
|
|
17
|
+
highlightOnHover: true,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (purpose === "sandbox") {
|
|
21
|
+
return {
|
|
22
|
+
purpose,
|
|
23
|
+
environmentPreset: "none",
|
|
24
|
+
showContactShadows: false,
|
|
25
|
+
showGrid: true,
|
|
26
|
+
clickToFocus: true,
|
|
27
|
+
highlightOnHover: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
purpose: "product",
|
|
32
|
+
environmentPreset: "studio",
|
|
33
|
+
showContactShadows: true,
|
|
34
|
+
showGrid: false,
|
|
35
|
+
clickToFocus: true,
|
|
36
|
+
highlightOnHover: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function purposePresetPatch(purpose: Scene3dPurpose): Partial<Scene3dConfig> {
|
|
41
|
+
const presentation = createDefaultPresentation(purpose);
|
|
42
|
+
if (purpose === "product") {
|
|
43
|
+
return {
|
|
44
|
+
presentation,
|
|
45
|
+
lighting: {
|
|
46
|
+
ambientIntensity: 0.35,
|
|
47
|
+
directionalIntensity: 1.4,
|
|
48
|
+
backgroundColor: "#0f1115",
|
|
49
|
+
},
|
|
50
|
+
orbit: {
|
|
51
|
+
enableMouseControl: true,
|
|
52
|
+
autoRotate: false,
|
|
53
|
+
autoRotateSpeed: 0.6,
|
|
54
|
+
enableDamping: true,
|
|
55
|
+
},
|
|
56
|
+
enableShadows: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (purpose === "twin") {
|
|
60
|
+
return {
|
|
61
|
+
presentation,
|
|
62
|
+
lighting: {
|
|
63
|
+
ambientIntensity: 0.5,
|
|
64
|
+
directionalIntensity: 1.05,
|
|
65
|
+
backgroundColor: "#12161c",
|
|
66
|
+
},
|
|
67
|
+
orbit: {
|
|
68
|
+
enableMouseControl: true,
|
|
69
|
+
autoRotate: false,
|
|
70
|
+
autoRotateSpeed: 0.8,
|
|
71
|
+
enableDamping: true,
|
|
72
|
+
},
|
|
73
|
+
enableShadows: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
presentation,
|
|
78
|
+
lighting: {
|
|
79
|
+
ambientIntensity: 0.7,
|
|
80
|
+
directionalIntensity: 0.9,
|
|
81
|
+
backgroundColor: "#1a1f27",
|
|
82
|
+
},
|
|
83
|
+
orbit: {
|
|
84
|
+
enableMouseControl: true,
|
|
85
|
+
autoRotate: false,
|
|
86
|
+
autoRotateSpeed: 0.8,
|
|
87
|
+
enableDamping: true,
|
|
88
|
+
},
|
|
89
|
+
enableShadows: false,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createDefaultScene3dConfig(): Scene3dConfig {
|
|
94
|
+
return {
|
|
95
|
+
models: [],
|
|
96
|
+
camera: {
|
|
97
|
+
position: [4, 3, 6],
|
|
98
|
+
target: [0, 0.5, 0],
|
|
99
|
+
fov: 45,
|
|
100
|
+
near: 0.1,
|
|
101
|
+
far: 200,
|
|
102
|
+
},
|
|
103
|
+
orbit: {
|
|
104
|
+
enableMouseControl: true,
|
|
105
|
+
autoRotate: false,
|
|
106
|
+
autoRotateSpeed: 0.6,
|
|
107
|
+
enableDamping: true,
|
|
108
|
+
},
|
|
109
|
+
lighting: {
|
|
110
|
+
ambientIntensity: 0.35,
|
|
111
|
+
directionalIntensity: 1.4,
|
|
112
|
+
backgroundColor: "#0f1115",
|
|
113
|
+
},
|
|
114
|
+
presentation: createDefaultPresentation("product"),
|
|
115
|
+
modelAnimations: [],
|
|
116
|
+
cameraAnimations: [],
|
|
117
|
+
enableShadows: true,
|
|
118
|
+
hiddenObjectKeys: [],
|
|
119
|
+
soloObjectKey: null,
|
|
120
|
+
selectedObjectKey: null,
|
|
121
|
+
objectTags: [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function mergeScene3dConfig(
|
|
126
|
+
partial?: Partial<Scene3dConfig> | null
|
|
127
|
+
): Scene3dConfig {
|
|
128
|
+
const base = createDefaultScene3dConfig();
|
|
129
|
+
if (!partial) return base;
|
|
130
|
+
return {
|
|
131
|
+
...base,
|
|
132
|
+
...partial,
|
|
133
|
+
camera: { ...base.camera, ...partial.camera },
|
|
134
|
+
orbit: { ...base.orbit, ...partial.orbit },
|
|
135
|
+
lighting: { ...base.lighting, ...partial.lighting },
|
|
136
|
+
presentation: { ...base.presentation, ...partial.presentation },
|
|
137
|
+
models: partial.models ?? base.models,
|
|
138
|
+
modelAnimations: partial.modelAnimations ?? base.modelAnimations,
|
|
139
|
+
cameraAnimations: partial.cameraAnimations ?? base.cameraAnimations,
|
|
140
|
+
hiddenObjectKeys: partial.hiddenObjectKeys ?? base.hiddenObjectKeys,
|
|
141
|
+
soloObjectKey:
|
|
142
|
+
partial.soloObjectKey === undefined ? base.soloObjectKey : partial.soloObjectKey,
|
|
143
|
+
selectedObjectKey:
|
|
144
|
+
partial.selectedObjectKey === undefined
|
|
145
|
+
? base.selectedObjectKey
|
|
146
|
+
: partial.selectedObjectKey,
|
|
147
|
+
objectTags: partial.objectTags ?? base.objectTags,
|
|
148
|
+
};
|
|
149
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Object3D } from "three";
|
|
2
|
+
|
|
3
|
+
/** 收集 GLTF 场景中所有具名节点(mesh/group),供配置面板选择 */
|
|
4
|
+
export function collectObjectNames(root: Object3D): string[] {
|
|
5
|
+
const names = new Set<string>();
|
|
6
|
+
root.traverse((obj) => {
|
|
7
|
+
if (obj.name && obj.name.trim()) {
|
|
8
|
+
names.add(obj.name.trim());
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
return Array.from(names).sort((a, b) => a.localeCompare(b));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function inferModelFormat(fileName: string): "glb" | "gltf" | "obj" | "fbx" | "unknown" {
|
|
15
|
+
const lower = fileName.toLowerCase();
|
|
16
|
+
if (lower.endsWith(".glb")) return "glb";
|
|
17
|
+
if (lower.endsWith(".gltf")) return "gltf";
|
|
18
|
+
if (lower.endsWith(".obj")) return "obj";
|
|
19
|
+
if (lower.endsWith(".fbx")) return "fbx";
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const SCENE3D_OBJECT_KEY_SEP = "|||";
|
|
2
|
+
|
|
3
|
+
export function makeScene3dObjectKey(modelId: string, objectName: string): string {
|
|
4
|
+
return `${modelId}${SCENE3D_OBJECT_KEY_SEP}${objectName}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function parseScene3dObjectKey(
|
|
8
|
+
key: string | null | undefined
|
|
9
|
+
): { modelId: string; objectName: string } | null {
|
|
10
|
+
if (!key) return null;
|
|
11
|
+
const index = key.indexOf(SCENE3D_OBJECT_KEY_SEP);
|
|
12
|
+
if (index < 0) return null;
|
|
13
|
+
return {
|
|
14
|
+
modelId: key.slice(0, index),
|
|
15
|
+
objectName: key.slice(index + SCENE3D_OBJECT_KEY_SEP.length),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isScene3dObjectVisible(options: {
|
|
20
|
+
modelId: string;
|
|
21
|
+
objectName: string;
|
|
22
|
+
hiddenObjectKeys?: string[];
|
|
23
|
+
soloObjectKey?: string | null;
|
|
24
|
+
}): boolean {
|
|
25
|
+
const key = makeScene3dObjectKey(options.modelId, options.objectName);
|
|
26
|
+
if (options.soloObjectKey) return options.soloObjectKey === key;
|
|
27
|
+
return !options.hiddenObjectKeys?.includes(key);
|
|
28
|
+
}
|
package/src/pivot.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Box3, Vector3 } from "three";
|
|
2
|
+
import type { Scene3dPivot } from "./types";
|
|
3
|
+
|
|
4
|
+
export function getPivotWorldPoint(box: Box3, pivot: Scene3dPivot): Vector3 {
|
|
5
|
+
const center = new Vector3();
|
|
6
|
+
box.getCenter(center);
|
|
7
|
+
const { min, max } = box;
|
|
8
|
+
switch (pivot) {
|
|
9
|
+
case "left":
|
|
10
|
+
return new Vector3(min.x, center.y, center.z);
|
|
11
|
+
case "right":
|
|
12
|
+
return new Vector3(max.x, center.y, center.z);
|
|
13
|
+
case "front":
|
|
14
|
+
return new Vector3(center.x, center.y, max.z);
|
|
15
|
+
case "back":
|
|
16
|
+
return new Vector3(center.x, center.y, min.z);
|
|
17
|
+
case "top":
|
|
18
|
+
return new Vector3(center.x, max.y, center.z);
|
|
19
|
+
case "bottom":
|
|
20
|
+
return new Vector3(center.x, min.y, center.z);
|
|
21
|
+
case "center":
|
|
22
|
+
default:
|
|
23
|
+
return center.clone();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function rotationSign(direction: "clockwise" | "counterclockwise"): number {
|
|
28
|
+
return direction === "clockwise" ? -1 : 1;
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { A11y, A11yAnnouncer } from "@react-three/a11y";
|
|
3
|
+
|
|
4
|
+
type Scene3dA11yProps = {
|
|
5
|
+
description: string;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** 为 Canvas 内场景提供基础无障碍语义与播报。 */
|
|
10
|
+
export function Scene3dA11y({ description, children }: Scene3dA11yProps) {
|
|
11
|
+
return (
|
|
12
|
+
<A11y role="contentinfo" description={description}>
|
|
13
|
+
{children}
|
|
14
|
+
</A11y>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A11yAnnouncer 会渲染 DOM,必须放在 Canvas 外。 */
|
|
19
|
+
export function Scene3dA11yAnnouncer() {
|
|
20
|
+
return (
|
|
21
|
+
<A11yAnnouncer />
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Scene3dPurpose } from "../types";
|
|
2
|
+
|
|
3
|
+
type Scene3dEffectsProps = {
|
|
4
|
+
purpose: Scene3dPurpose;
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 后处理会在切换场景用途时挂载/卸载 EffectComposer,容易把画布尺寸量错、画面缩到一角变空白。
|
|
10
|
+
* 产品展示的质感改由灯光和环境贴图承担,这里保持空实现以免切用途闪空白。
|
|
11
|
+
*/
|
|
12
|
+
export function Scene3dEffects(_props: Scene3dEffectsProps) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|