editor-sdk 0.1.4 → 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.
@@ -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
- // import { PrimitiveProps } from '@react-three/fiber';
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 CACHE_duration_MS = 50 * 60 * 1000; // 50 minutes (S3 URLs last 60m)
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
- return _jsx("primitive", { object: scene, ...props });
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 [modelUrl, setModelUrl] = useState(null);
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
- const cached = localStorage.getItem(cacheKey);
28
- if (cached) {
29
- const data = JSON.parse(cached);
30
- const age = Date.now() - (data.timestamp || 0);
31
- if (age < CACHE_duration_MS) {
32
- if (mounted) {
33
- console.log(`[SecureScene] Using cached URL for ${id}`);
34
- setModelUrl(data.modelUrl);
35
- onLoad?.();
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 the Registry API
45
- const res = await fetch(`${registryUrl}/api/registry/${id}`, {
46
- headers: {
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
- if (!data.modelUrl || data.modelUrl === 'none') {
57
- throw new Error("No model URL found for this component");
58
- }
59
- // 2. Cache it
84
+ // Cache it
60
85
  try {
61
- const cachePayload = { ...data, timestamp: Date.now() };
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
- setModelUrl(data.modelUrl);
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
- setError(err.message);
76
- onError?.(err);
100
+ if (!force) {
101
+ setError(err.message);
102
+ onError?.(err);
103
+ }
77
104
  }
78
105
  }
79
106
  }
80
107
  fetchMetadata();
81
- return () => { mounted = false; };
82
- }, [id, registryUrl, token, onLoad, onError]);
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 (_jsxs("group", { children: [_jsxs("mesh", { children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: "red", wireframe: true })] }), _jsx(Html, { position: [0, 1, 0], center: true, children: _jsxs("div", { style: { color: 'red', background: 'white', padding: 4 }, children: ["Error: ", error] }) })] }));
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 (!modelUrl) {
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 (_jsxs("group", { children: [_jsx(Suspense, { fallback: null, children: _jsx(SceneLoader, { url: modelUrl, ...props }) }), isDev && (_jsx(Html, { position: [0, 0, 0], fullscreen: true, style: { pointerEvents: 'none' }, children: _jsx("div", { style: {
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
- .action(async () => {
40
- console.log(chalk.blue('šŸ” Authenticating with 3D Editor...'));
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 < 5 ? 'API Key seems too short' : true
46
+ validate: value => value.length < 1 ? 'API Key cannot be empty' : true
46
47
  });
47
48
  if (response.apiKey) {
48
- saveConfig({ apiKey: response.apiKey });
49
- console.log(chalk.green('āœ“ Logged in successfully!'));
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.4",
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",