editor-sdk 0.1.5 ā 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/SecureScene.js +100 -62
- package/dist/cli.js +55 -7
- package/package.json +3 -1
package/dist/SecureScene.js
CHANGED
|
@@ -1,102 +1,140 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState, Suspense } from 'react';
|
|
2
|
+
import { useEffect, useState, Suspense, useRef } from 'react';
|
|
3
3
|
import { useGLTF, Html } from '@react-three/drei';
|
|
4
|
-
|
|
4
|
+
import { useFrame } from '@react-three/fiber';
|
|
5
5
|
// Default Registry URL (can be overridden)
|
|
6
6
|
const DEFAULT_REGISTRY_URL = 'http://localhost:3000';
|
|
7
|
-
const
|
|
7
|
+
const CACHE_DURATION_MS = 50 * 60 * 1000; // 50 minutes
|
|
8
|
+
// --- Internal Animator (Copied from Editor) ---
|
|
9
|
+
function SecureAnimator({ children, animation, ...props }) {
|
|
10
|
+
const ref = useRef(null);
|
|
11
|
+
useFrame((state, delta) => {
|
|
12
|
+
if (!ref.current || !animation || animation.type === 'none')
|
|
13
|
+
return;
|
|
14
|
+
const speed = animation.speed || 1;
|
|
15
|
+
const intensity = animation.intensity || 1;
|
|
16
|
+
const axis = animation.axis || 'y';
|
|
17
|
+
// Time based animations
|
|
18
|
+
const t = state.clock.getElapsedTime();
|
|
19
|
+
switch (animation.type) {
|
|
20
|
+
case 'rotate':
|
|
21
|
+
ref.current.rotation[axis] += delta * speed;
|
|
22
|
+
break;
|
|
23
|
+
case 'float':
|
|
24
|
+
const startY = 0; // Relative to parent
|
|
25
|
+
ref.current.position.y = startY + Math.sin(t * speed) * (0.5 * intensity);
|
|
26
|
+
break;
|
|
27
|
+
case 'pulse':
|
|
28
|
+
const scaleBase = 1;
|
|
29
|
+
const scale = scaleBase + Math.sin(t * speed * 2) * (0.1 * intensity);
|
|
30
|
+
ref.current.scale.set(scale, scale, scale);
|
|
31
|
+
break;
|
|
32
|
+
case 'bounce':
|
|
33
|
+
ref.current.position.y = Math.abs(Math.sin(t * speed * 2)) * intensity;
|
|
34
|
+
break;
|
|
35
|
+
case 'shake':
|
|
36
|
+
ref.current.position.x = Math.sin(t * speed * 10) * (0.05 * intensity);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return _jsx("group", { ref: ref, ...props, children: children });
|
|
41
|
+
}
|
|
42
|
+
// Loader wrapper
|
|
8
43
|
function SceneLoader({ url, ...props }) {
|
|
9
|
-
// useGLTF will suspend automatically
|
|
10
44
|
const { scene } = useGLTF(url);
|
|
11
|
-
|
|
45
|
+
// Clone scene to avoid shared state issues if reused
|
|
46
|
+
const cloned = scene.clone();
|
|
47
|
+
return _jsx("primitive", { object: cloned, ...props });
|
|
12
48
|
}
|
|
13
49
|
export function SecureScene({ id, registryUrl = DEFAULT_REGISTRY_URL, token, onLoad, onError, ...props }) {
|
|
14
|
-
const [
|
|
50
|
+
const [metadata, setMetadata] = useState(null);
|
|
15
51
|
const [error, setError] = useState(null);
|
|
16
52
|
const [isDev, setIsDev] = useState(false);
|
|
17
53
|
useEffect(() => {
|
|
18
54
|
let mounted = true;
|
|
19
|
-
// Check Dev Environment
|
|
20
55
|
if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
|
|
21
56
|
setIsDev(true);
|
|
22
57
|
}
|
|
23
|
-
async function fetchMetadata() {
|
|
58
|
+
async function fetchMetadata(force = false) {
|
|
24
59
|
try {
|
|
25
|
-
// 0. Check Cache (LocalStorage)
|
|
26
60
|
const cacheKey = `3d-editor:cache:${id}`;
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
61
|
+
// 0. Check Cache (if not forced)
|
|
62
|
+
if (!force) {
|
|
63
|
+
const cached = localStorage.getItem(cacheKey);
|
|
64
|
+
if (cached) {
|
|
65
|
+
const data = JSON.parse(cached);
|
|
66
|
+
const age = Date.now() - (data.timestamp || 0);
|
|
67
|
+
if (age < CACHE_DURATION_MS) {
|
|
68
|
+
if (mounted) {
|
|
69
|
+
setMetadata(data);
|
|
70
|
+
onLoad?.();
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
localStorage.removeItem(cacheKey);
|
|
36
76
|
}
|
|
37
|
-
return; // Exit early
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
console.log(`[SecureScene] Cache expired for ${id}`);
|
|
41
|
-
localStorage.removeItem(cacheKey);
|
|
42
77
|
}
|
|
43
78
|
}
|
|
44
|
-
// 1. Call
|
|
45
|
-
const res = await fetch(`${registryUrl}/api/registry/${id}
|
|
46
|
-
|
|
47
|
-
// Future: 'Authorization': `Bearer ${token}`
|
|
48
|
-
'Content-Type': 'application/json',
|
|
49
|
-
// Referer is sent automatically by browser
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
if (!res.ok) {
|
|
79
|
+
// 1. Call Registry
|
|
80
|
+
const res = await fetch(`${registryUrl}/api/registry/${id}`);
|
|
81
|
+
if (!res.ok)
|
|
53
82
|
throw new Error(`Registry Error: ${res.statusText}`);
|
|
54
|
-
}
|
|
55
83
|
const data = await res.json();
|
|
56
|
-
|
|
57
|
-
throw new Error("No model URL found for this component");
|
|
58
|
-
}
|
|
59
|
-
// 2. Cache it
|
|
84
|
+
// Cache it
|
|
60
85
|
try {
|
|
61
|
-
|
|
62
|
-
localStorage.setItem(cacheKey, JSON.stringify(cachePayload));
|
|
63
|
-
}
|
|
64
|
-
catch (e) {
|
|
65
|
-
// Ignore quota errors
|
|
86
|
+
localStorage.setItem(cacheKey, JSON.stringify({ ...data, timestamp: Date.now() }));
|
|
66
87
|
}
|
|
88
|
+
catch (e) { }
|
|
67
89
|
if (mounted) {
|
|
68
|
-
|
|
90
|
+
// Only update if changed? React handles basic diff, but for animations it's fine.
|
|
91
|
+
setMetadata(data);
|
|
69
92
|
onLoad?.();
|
|
70
93
|
}
|
|
71
94
|
}
|
|
72
95
|
catch (err) {
|
|
73
96
|
if (mounted) {
|
|
97
|
+
// Only log error if not polling (to avoid spam) or if critical?
|
|
98
|
+
// For now, log.
|
|
74
99
|
console.error("SecureScene Error:", err);
|
|
75
|
-
|
|
76
|
-
|
|
100
|
+
if (!force) {
|
|
101
|
+
setError(err.message);
|
|
102
|
+
onError?.(err);
|
|
103
|
+
}
|
|
77
104
|
}
|
|
78
105
|
}
|
|
79
106
|
}
|
|
80
107
|
fetchMetadata();
|
|
81
|
-
|
|
82
|
-
|
|
108
|
+
let interval;
|
|
109
|
+
// Poll every 2 seconds if on localhost
|
|
110
|
+
if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
|
|
111
|
+
interval = setInterval(() => {
|
|
112
|
+
fetchMetadata(true); // Force refresh
|
|
113
|
+
}, 2000);
|
|
114
|
+
}
|
|
115
|
+
return () => {
|
|
116
|
+
mounted = false;
|
|
117
|
+
if (interval)
|
|
118
|
+
clearInterval(interval);
|
|
119
|
+
};
|
|
120
|
+
}, [id, registryUrl, onLoad, onError]);
|
|
83
121
|
if (error) {
|
|
84
|
-
return (
|
|
122
|
+
return (_jsx("group", { children: _jsx(Html, { position: [0, 1, 0], center: true, children: _jsxs("div", { style: { color: 'red', background: 'white', padding: 4, border: '1px solid red' }, children: ["Error: ", error] }) }) }));
|
|
85
123
|
}
|
|
86
|
-
if (!
|
|
87
|
-
// Loading State
|
|
124
|
+
if (!metadata)
|
|
88
125
|
return null;
|
|
126
|
+
// Render Logic
|
|
127
|
+
// Extract animation from props if it exists, to apply to the top-level container
|
|
128
|
+
const { animation, ...restProps } = props;
|
|
129
|
+
// 1. Multi-Model Support (sceneData)
|
|
130
|
+
if (metadata.sceneData && metadata.sceneData.objects) {
|
|
131
|
+
return (_jsx(SecureAnimator, { animation: animation, ...restProps, children: metadata.sceneData.objects.map((obj, index) => (_jsx("group", { position: obj.position || [0, 0, 0], rotation: obj.rotation || [0, 0, 0], scale: obj.scale || [1, 1, 1], children: _jsx(SecureAnimator, { animation: obj.animation, children: obj.type === 'imported' ? (_jsx(Suspense, { fallback: null, children: _jsx(SceneLoader, { url: obj.modelUrl || obj.modelPath }) })) : (
|
|
132
|
+
// Basic Primitive Support
|
|
133
|
+
_jsxs("mesh", { children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: obj.material?.color || "gray" })] })) }) }, `${obj.id}-${index}`))) }));
|
|
134
|
+
}
|
|
135
|
+
// 2. Legacy / Single Model Fallback
|
|
136
|
+
if (metadata.modelUrl && metadata.modelUrl !== 'none') {
|
|
137
|
+
return (_jsx(SecureAnimator, { animation: animation, ...restProps, children: _jsx(Suspense, { fallback: null, children: _jsx(SceneLoader, { url: metadata.modelUrl }) }) }));
|
|
89
138
|
}
|
|
90
|
-
return
|
|
91
|
-
position: 'absolute',
|
|
92
|
-
bottom: 10,
|
|
93
|
-
right: 10,
|
|
94
|
-
background: 'rgba(0,0,0,0.7)',
|
|
95
|
-
color: '#0f0',
|
|
96
|
-
padding: '4px 8px',
|
|
97
|
-
borderRadius: 4,
|
|
98
|
-
fontSize: '10px',
|
|
99
|
-
fontFamily: 'monospace',
|
|
100
|
-
border: '1px solid #0f0'
|
|
101
|
-
}, children: "DEV MODE: SECURE" }) }))] }));
|
|
139
|
+
return null;
|
|
102
140
|
}
|
package/dist/cli.js
CHANGED
|
@@ -36,17 +36,37 @@ program
|
|
|
36
36
|
program
|
|
37
37
|
.command('login')
|
|
38
38
|
.description('Authenticate with the 3D Editor Registry')
|
|
39
|
-
.
|
|
40
|
-
|
|
39
|
+
.option('--host <url>', 'Registry Host URL', 'http://localhost:3000')
|
|
40
|
+
.action(async (options) => {
|
|
41
|
+
console.log(chalk.blue('š Authenticating with 3D Editor... (v0.1.6)'));
|
|
41
42
|
const response = await prompts({
|
|
42
43
|
type: 'password',
|
|
43
44
|
name: 'apiKey',
|
|
44
45
|
message: 'Enter your API Key:',
|
|
45
|
-
validate: value => value.length <
|
|
46
|
+
validate: value => value.length < 1 ? 'API Key cannot be empty' : true
|
|
46
47
|
});
|
|
47
48
|
if (response.apiKey) {
|
|
48
|
-
|
|
49
|
-
console.log(chalk.
|
|
49
|
+
const host = options.host;
|
|
50
|
+
console.log(chalk.gray(`Connecting to ${host}...`));
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(`${host}/api/registry/verify`, {
|
|
53
|
+
headers: { 'Authorization': `Bearer ${response.apiKey}` }
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
console.error(chalk.red('\nā Login Failed: Invalid API Key.'));
|
|
57
|
+
console.error(chalk.yellow('Please check your key and try again.'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const data = await res.json();
|
|
61
|
+
console.log(chalk.green(`\nā Verified as ${data.user.email}`));
|
|
62
|
+
saveConfig({ apiKey: response.apiKey });
|
|
63
|
+
console.log(chalk.green('ā Logged in successfully!'));
|
|
64
|
+
console.log(chalk.gray(`Credentials saved to ${CONFIG_FILE}`));
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error(chalk.red('\nā Connection Failed:'), err.message);
|
|
68
|
+
console.error(chalk.yellow(`Could not reach ${host}`));
|
|
69
|
+
}
|
|
50
70
|
}
|
|
51
71
|
else {
|
|
52
72
|
console.log(chalk.yellow('Login cancelled.'));
|
|
@@ -57,6 +77,7 @@ program
|
|
|
57
77
|
.command('add <componentId>')
|
|
58
78
|
.description('Add a component by ID')
|
|
59
79
|
.option('-p, --package', 'Install as a node_module package', false)
|
|
80
|
+
.option('-f, --force', 'Force overwrite of existing files', false)
|
|
60
81
|
.option('--host <url>', 'Registry Host URL', 'http://localhost:3000') // Default to local for dev
|
|
61
82
|
.action(async (componentId, options) => {
|
|
62
83
|
try {
|
|
@@ -82,11 +103,25 @@ program
|
|
|
82
103
|
headers: apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {}
|
|
83
104
|
});
|
|
84
105
|
if (!res.ok) {
|
|
106
|
+
const errorBody = await res.text();
|
|
107
|
+
try {
|
|
108
|
+
const json = JSON.parse(errorBody);
|
|
109
|
+
if (json.details) {
|
|
110
|
+
console.error(chalk.red(`Registry Error: ${json.error}`));
|
|
111
|
+
console.error(chalk.red(`Details: ${json.details}`));
|
|
112
|
+
if (json.stack)
|
|
113
|
+
console.error(chalk.gray(json.stack));
|
|
114
|
+
throw new Error("Remote operation failed");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
// Not JSON or parse error, ignore
|
|
119
|
+
}
|
|
85
120
|
if (res.status === 404)
|
|
86
121
|
throw new Error(`Component '${componentId}' not found.`);
|
|
87
122
|
if (res.status === 401)
|
|
88
123
|
throw new Error(`Unauthorized. Please login.`);
|
|
89
|
-
throw new Error(`Registry Error: ${res.statusText}`);
|
|
124
|
+
throw new Error(`Registry Error: ${res.statusText} - ${errorBody.slice(0, 100)}`);
|
|
90
125
|
}
|
|
91
126
|
const data = await res.json();
|
|
92
127
|
const { name, id } = data; // We don't need assets/code/etc anymore for the shell
|
|
@@ -146,7 +181,21 @@ program
|
|
|
146
181
|
else if (!fs.existsSync(path.dirname(targetFile))) {
|
|
147
182
|
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
|
|
148
183
|
}
|
|
184
|
+
// Check if file exists and ask for permission if not forced
|
|
185
|
+
if (fs.existsSync(targetFile) && !options.force) {
|
|
186
|
+
const { overwrite } = await prompts({
|
|
187
|
+
type: 'confirm',
|
|
188
|
+
name: 'overwrite',
|
|
189
|
+
message: `File ${componentName}.tsx already exists. Overwrite?`,
|
|
190
|
+
initial: false
|
|
191
|
+
});
|
|
192
|
+
if (!overwrite) {
|
|
193
|
+
console.log(chalk.yellow('ā¹ Action cancelled. File was not modified.'));
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
149
197
|
// The Shell Code
|
|
198
|
+
// REMOVED registryUrl explicit prop to allow default or parent override
|
|
150
199
|
const shellCode = `import React from 'react';
|
|
151
200
|
import { SecureScene } from 'editor-sdk';
|
|
152
201
|
|
|
@@ -154,7 +203,6 @@ export default function ${componentName}(props: any) {
|
|
|
154
203
|
return (
|
|
155
204
|
<SecureScene
|
|
156
205
|
id="${id}"
|
|
157
|
-
registryUrl="${options.host}"
|
|
158
206
|
{...props}
|
|
159
207
|
/>
|
|
160
208
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "editor-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"dist"
|
|
29
29
|
],
|
|
30
30
|
"devDependencies": {
|
|
31
|
+
"@react-three/drei": ">=9.0.0",
|
|
32
|
+
"@react-three/fiber": ">=8.0.0",
|
|
31
33
|
"@types/react": "^18.3.27",
|
|
32
34
|
"@types/three": "^0.160.0",
|
|
33
35
|
"@types/node": "^20.0.0",
|