From 88932c77b47c840cb09158d1fb1a7c67da255ea8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Jul 2025 08:56:43 +0000 Subject: [PATCH 1/2] Add 3D isometric traffic control with React Three Fiber Co-authored-by: deslittle --- .../ui/app/components/isometric/Isometric.tsx | 573 ++++++++++++++++++ apps/ui/app/components/isometric/index.ts | 4 + apps/ui/app/components/isometric/presets.ts | 355 +++++++++++ apps/ui/app/components/isometric/types.ts | 270 +++++++++ apps/ui/app/components/isometric/utils.ts | 301 +++++++++ apps/ui/app/hooks/useESBuildWorker.ts | 150 ++++- .../control/IsometricTrafficControl.tsx | 251 ++++++++ apps/ui/examples/example-1/control/README.md | 118 ++++ .../example-1/control/SimpleIsometricDemo.tsx | 121 ++++ .../control/traffic-intersection.json | 479 +++++++++++++++ apps/ui/package.json | 4 + apps/ui/pnpm-lock.yaml | 513 ++++++++++++++++ 12 files changed, 3115 insertions(+), 24 deletions(-) create mode 100644 apps/ui/app/components/isometric/Isometric.tsx create mode 100644 apps/ui/app/components/isometric/index.ts create mode 100644 apps/ui/app/components/isometric/presets.ts create mode 100644 apps/ui/app/components/isometric/types.ts create mode 100644 apps/ui/app/components/isometric/utils.ts create mode 100644 apps/ui/examples/example-1/control/IsometricTrafficControl.tsx create mode 100644 apps/ui/examples/example-1/control/README.md create mode 100644 apps/ui/examples/example-1/control/SimpleIsometricDemo.tsx create mode 100644 apps/ui/examples/example-1/control/traffic-intersection.json diff --git a/apps/ui/app/components/isometric/Isometric.tsx b/apps/ui/app/components/isometric/Isometric.tsx new file mode 100644 index 0000000..8bb4882 --- /dev/null +++ b/apps/ui/app/components/isometric/Isometric.tsx @@ -0,0 +1,573 @@ +import React, { useRef, useState, useEffect, useCallback, useMemo } from 'react'; +import { Canvas, useFrame, useThree } from '@react-three/fiber'; +import { + OrbitControls, + Environment, + Grid, + Stats, + Html, + Text, + Box, + Cylinder, + Sphere, + Cone, + Plane +} from '@react-three/drei'; +import * as THREE from 'three'; +import { + IsometricProps, + IsometricAPI, + IsometricNode, + Connection, + LiveDataUpdate, + Vector3, + Color, + Transform +} from './types'; + +// Utility functions +const colorToThree = (color?: Color): THREE.Color => { + if (!color) return new THREE.Color(0x888888); + return new THREE.Color(color.r / 255, color.g / 255, color.b / 255); +}; + +const vector3ToThree = (vec: Vector3): THREE.Vector3 => { + return new THREE.Vector3(vec.x, vec.y, vec.z); +}; + +// Individual node renderer component +const IsometricNodeRenderer: React.FC<{ + node: IsometricNode; + liveData?: Record; + onNodeClick?: (nodeId: string, event: any) => void; + onNodeHover?: (nodeId: string, isHovering: boolean, event: any) => void; +}> = ({ node, liveData, onNodeClick, onNodeHover }) => { + const meshRef = useRef(null); + const [isHovered, setIsHovered] = useState(false); + + // Apply live data transforms + const processedNode = useMemo(() => { + if (!node.liveData || !liveData) return node; + + let updatedNode = { ...node }; + + node.liveData.forEach(binding => { + const value = liveData[binding.variableName]; + if (value === undefined) return; + + // Apply transform if specified + let transformedValue = value; + if (binding.transform) { + const { transform } = binding; + switch (transform.type) { + case 'linear': + if (transform.inputRange && transform.outputRange) { + const [inMin, inMax] = transform.inputRange; + const [outMin, outMax] = transform.outputRange; + const ratio = (value - inMin) / (inMax - inMin); + transformedValue = outMin + ratio * (outMax - outMin); + } + break; + case 'step': + if (transform.steps) { + const step = transform.steps.find(s => s.input === value) || + transform.steps.reduce((prev, curr) => + Math.abs(curr.input - value) < Math.abs(prev.input - value) ? curr : prev + ); + transformedValue = step.output; + } + break; + case 'custom': + if (transform.customFunction) { + try { + const func = new Function('value', 'node', 'liveData', transform.customFunction); + transformedValue = func(value, node, liveData); + } catch (e) { + console.warn('Error executing custom transform function:', e); + } + } + break; + } + } + + // Apply the transformed value to the node property + const propertyPath = binding.property.split('.'); + let current = updatedNode as any; + for (let i = 0; i < propertyPath.length - 1; i++) { + if (!current[propertyPath[i]]) { + current[propertyPath[i]] = {}; + } + current = current[propertyPath[i]]; + } + current[propertyPath[propertyPath.length - 1]] = transformedValue; + }); + + return updatedNode; + }, [node, liveData]); + + const handleClick = useCallback((event: any) => { + event.stopPropagation(); + onNodeClick?.(node.id, event); + }, [node.id, onNodeClick]); + + const handlePointerOver = useCallback((event: any) => { + event.stopPropagation(); + setIsHovered(true); + onNodeHover?.(node.id, true, event); + }, [node.id, onNodeHover]); + + const handlePointerOut = useCallback((event: any) => { + event.stopPropagation(); + setIsHovered(false); + onNodeHover?.(node.id, false, event); + }, [node.id, onNodeHover]); + + // Animation frame loop for node animations + useFrame((state, delta) => { + if (!meshRef.current || !processedNode.animations) return; + + processedNode.animations.forEach(animation => { + if (!animation.autoplay) return; + + // Simple animation implementation - can be enhanced + const time = state.clock.getElapsedTime(); + const duration = animation.duration || 1; + const progress = (time % duration) / duration; + + // Basic linear interpolation between keyframes + if (animation.keyframes.length >= 2) { + const fromFrame = animation.keyframes[0]; + const toFrame = animation.keyframes[1]; + const value = fromFrame.value + (toFrame.value - fromFrame.value) * progress; + + // Apply animation value - simplified implementation + if (animation.property.includes('position')) { + if (animation.property.endsWith('.y')) { + meshRef.current.position.y = value; + } + // Add more position properties as needed + } + } + }); + }); + + const material = useMemo(() => { + const style = processedNode.style; + const materialType = style?.material || 'standard'; + + const materialProps = { + color: colorToThree(style?.color), + transparent: style?.opacity !== undefined, + opacity: style?.opacity ?? 1, + wireframe: style?.wireframe ?? false, + visible: style?.visible ?? true, + }; + + switch (materialType) { + case 'basic': + return ; + case 'lambert': + return ; + case 'phong': + return ; + default: + return ; + } + }, [processedNode.style, isHovered]); + + const geometry = processedNode.geometry || {}; + const transform = processedNode.transform; + + const commonProps = { + ref: meshRef, + position: [transform.position.x, transform.position.y, transform.position.z] as [number, number, number], + rotation: [transform.rotation.x, transform.rotation.y, transform.rotation.z] as [number, number, number], + scale: [transform.scale.x, transform.scale.y, transform.scale.z] as [number, number, number], + onClick: processedNode.clickable ? handleClick : undefined, + onPointerOver: processedNode.hoverable ? handlePointerOver : undefined, + onPointerOut: processedNode.hoverable ? handlePointerOut : undefined, + }; + + // Render different node types + switch (processedNode.type) { + case 'box': + return ( + + {material} + + ); + + case 'cylinder': + return ( + + {material} + + ); + + case 'sphere': + return ( + + {material} + + ); + + case 'cone': + return ( + + {material} + + ); + + case 'plane': + return ( + + {material} + + ); + + case 'group': + return ( + + {processedNode.children?.map(child => ( + + ))} + + ); + + default: + return ( + + {material} + + ); + } +}; + +// Connection renderer component +const ConnectionRenderer: React.FC<{ + connection: Connection; + nodes: IsometricNode[]; + liveData?: Record; +}> = ({ connection, nodes, liveData }) => { + const lineRef = useRef(null); + + const fromNode = nodes.find(n => n.id === connection.fromNodeId); + const toNode = nodes.find(n => n.id === connection.toNodeId); + + if (!fromNode || !toNode) return null; + + const fromPos = vector3ToThree(fromNode.transform.position); + const toPos = vector3ToThree(toNode.transform.position); + + if (connection.fromPoint) { + fromPos.add(vector3ToThree(connection.fromPoint)); + } + if (connection.toPoint) { + toPos.add(vector3ToThree(connection.toPoint)); + } + + const points = [fromPos, toPos]; + + // Add curve height for curved connections + if (connection.type === 'curve' && connection.style?.curveHeight) { + const midPoint = fromPos.clone().lerp(toPos, 0.5); + midPoint.y += connection.style.curveHeight; + points.splice(1, 0, midPoint); + } + + const geometry = new THREE.BufferGeometry().setFromPoints(points); + + return ( + + + + ); +}; + +// Camera controller for view modes +const CameraController: React.FC<{ + viewMode: '2d' | '3d' | 'isometric'; + cameraConfig: any; +}> = ({ viewMode, cameraConfig }) => { + const { camera, gl } = useThree(); + + useEffect(() => { + const pos = cameraConfig.position; + const target = cameraConfig.target; + + switch (viewMode) { + case '2d': + camera.position.set(0, 10, 0); + camera.lookAt(0, 0, 0); + if (camera instanceof THREE.OrthographicCamera) { + camera.zoom = 1; + } + break; + case 'isometric': + camera.position.set(10, 10, 10); + camera.lookAt(0, 0, 0); + break; + case '3d': + default: + camera.position.set(pos.x, pos.y, pos.z); + camera.lookAt(target.x, target.y, target.z); + break; + } + + camera.updateProjectionMatrix(); + }, [viewMode, camera, cameraConfig]); + + return null; +}; + +// Main Isometric component +export const Isometric: React.FC = ({ + config, + plcVariables = {}, + onApiReady, + onNodeClick, + onNodeHover, + onViewModeChange, + className, + style +}) => { + const canvasRef = useRef(null); + const [viewMode, setViewMode] = useState<'2d' | '3d' | 'isometric'>(config.viewModes.default); + const [nodes, setNodes] = useState(config.nodes); + const [liveData, setLiveData] = useState>(plcVariables); + + // Update live data when plcVariables change + useEffect(() => { + setLiveData(plcVariables); + }, [plcVariables]); + + // Create API object + const api = useMemo(() => ({ + updateNode: (nodeId: string, updates: Partial) => { + setNodes(prev => prev.map(node => + node.id === nodeId ? { ...node, ...updates } : node + )); + }, + + addNode: (node: IsometricNode, parentId?: string) => { + if (parentId) { + setNodes(prev => prev.map(n => + n.id === parentId && n.children + ? { ...n, children: [...n.children, node] } + : n + )); + } else { + setNodes(prev => [...prev, node]); + } + }, + + removeNode: (nodeId: string) => { + setNodes(prev => prev.filter(node => node.id !== nodeId)); + }, + + getNode: (nodeId: string) => { + return nodes.find(node => node.id === nodeId) || null; + }, + + playAnimation: (nodeId: string, animationIndex?: number) => { + // Animation implementation would go here + }, + + pauseAnimation: (nodeId: string, animationIndex?: number) => { + // Animation implementation would go here + }, + + stopAnimation: (nodeId: string, animationIndex?: number) => { + // Animation implementation would go here + }, + + setViewMode: (mode: '2d' | '3d' | 'isometric') => { + setViewMode(mode); + onViewModeChange?.(mode); + }, + + setViewPreset: (presetName: string) => { + const preset = config.viewModes.presets.find(p => p.name === presetName); + if (preset) { + // Apply preset camera settings + } + }, + + setCameraPosition: (position: Vector3, target?: Vector3) => { + // Camera position implementation would go here + }, + + updateLiveData: (data: LiveDataUpdate) => { + const newLiveData = { ...liveData }; + data.variables.forEach(variable => { + newLiveData[variable.variableName] = variable.value; + }); + setLiveData(newLiveData); + }, + + subscribeTo: (variableNames: string[]) => { + // Subscription implementation would be handled by parent component + }, + + unsubscribeFrom: (variableNames: string[]) => { + // Unsubscription implementation would be handled by parent component + }, + + onNodeClick: (callback) => { + // Event handler setup + }, + + onNodeHover: (callback) => { + // Event handler setup + }, + + onViewModeChange: (callback) => { + // Event handler setup + }, + + exportToImage: async (options = {}) => { + if (canvasRef.current) { + return canvasRef.current.toDataURL(options.format === 'jpg' ? 'image/jpeg' : 'image/png'); + } + return ''; + }, + + exportToGLTF: async () => { + // GLTF export implementation would go here + return new Blob(); + }, + + reset: () => { + setNodes(config.nodes); + setViewMode(config.viewModes.default); + setLiveData({}); + } + }), [nodes, config, liveData, onViewModeChange]); + + // Expose API when component mounts + useEffect(() => { + onApiReady?.(api); + }, [api, onApiReady]); + + return ( +
+ {/* View mode controls */} + {config.ui?.showViewModeToggle && ( +
+ + + +
+ )} + + + {/* Camera controller */} + + + {/* Controls */} + {config.camera.enableControls && ( + + )} + + {/* Lighting */} + + + + {/* Environment */} + {config.scene.environment && config.scene.environment !== 'custom' && ( + + )} + + {/* Grid */} + {config.scene.grid?.enabled && ( + + )} + + {/* Nodes */} + {nodes.map(node => ( + + ))} + + {/* Connections */} + {config.connections.map(connection => ( + + ))} + + {/* Stats */} + {config.ui?.showStats && } + +
+ ); +}; + +export default Isometric; \ No newline at end of file diff --git a/apps/ui/app/components/isometric/index.ts b/apps/ui/app/components/isometric/index.ts new file mode 100644 index 0000000..e8fd8de --- /dev/null +++ b/apps/ui/app/components/isometric/index.ts @@ -0,0 +1,4 @@ +export { default as Isometric } from './Isometric'; +export * from './types'; +export * from './utils'; +export * from './presets'; \ No newline at end of file diff --git a/apps/ui/app/components/isometric/presets.ts b/apps/ui/app/components/isometric/presets.ts new file mode 100644 index 0000000..cd267d3 --- /dev/null +++ b/apps/ui/app/components/isometric/presets.ts @@ -0,0 +1,355 @@ +import { IsometricDiagramConfig } from './types'; +import { createTrafficLight, createIntersection } from './utils'; + +// Traffic Light Intersection Preset +export const trafficLightIntersection: IsometricDiagramConfig = { + id: 'traffic-light-intersection', + name: 'Traffic Light Intersection', + version: '1.0.0', + + scene: { + background: { r: 135, g: 206, b: 235 }, // Sky blue + environment: 'city', + fog: { + enabled: false, + color: { r: 200, g: 200, b: 200 }, + near: 10, + far: 50 + }, + grid: { + enabled: true, + size: 20, + divisions: 20, + color: { r: 100, g: 100, b: 100 } + } + }, + + camera: { + position: { x: 15, y: 15, z: 15 }, + target: { x: 0, y: 0, z: 0 }, + fov: 50, + near: 0.1, + far: 1000, + enableControls: true, + constrainPan: false, + constrainZoom: false, + minDistance: 5, + maxDistance: 50 + }, + + viewModes: { + default: 'isometric', + presets: [ + { + name: 'Isometric', + camera: { + position: { x: 15, y: 15, z: 15 }, + target: { x: 0, y: 0, z: 0 } + } + }, + { + name: 'Top Down', + camera: { + position: { x: 0, y: 30, z: 0 }, + target: { x: 0, y: 0, z: 0 } + }, + is2D: true + }, + { + name: 'Side View', + camera: { + position: { x: 30, y: 5, z: 0 }, + target: { x: 0, y: 5, z: 0 } + } + } + ] + }, + + nodes: createIntersection(), + + connections: [], + + liveDataConfig: { + enabled: true, + updateInterval: 100, + variables: [ + 'MainRoadRed', + 'MainRoadYellow', + 'MainRoadGreen', + 'SideRoadRed', + 'SideRoadYellow', + 'SideRoadGreen' + ] + }, + + ui: { + showViewModeToggle: true, + showControls: true, + showStats: false, + showGrid: true, + showAxes: false, + customControls: [ + { + id: 'reset-camera', + type: 'button', + label: 'Reset Camera', + action: 'resetCamera', + position: 'top-left' + } + ] + }, + + performance: { + shadows: true, + antialias: true, + pixelRatio: Math.min(window.devicePixelRatio, 2), + maxFramerate: 60 + } +}; + +// Industrial Plant Layout Preset +export const industrialPlant: IsometricDiagramConfig = { + id: 'industrial-plant', + name: 'Industrial Plant Layout', + version: '1.0.0', + + scene: { + background: { r: 200, g: 200, b: 200 }, + environment: 'warehouse', + grid: { + enabled: true, + size: 30, + divisions: 30, + color: { r: 150, g: 150, b: 150 } + } + }, + + camera: { + position: { x: 20, y: 20, z: 20 }, + target: { x: 0, y: 0, z: 0 }, + fov: 60, + near: 0.1, + far: 1000, + enableControls: true + }, + + viewModes: { + default: '3d', + presets: [ + { + name: 'Overview', + camera: { + position: { x: 25, y: 25, z: 25 }, + target: { x: 0, y: 0, z: 0 } + } + } + ] + }, + + nodes: [ + // Main building + { + id: 'main_building', + type: 'box', + name: 'Main Building', + transform: { + position: { x: 0, y: 5, z: 0 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 } + }, + geometry: { + width: 15, + height: 10, + depth: 20 + }, + style: { + color: { r: 150, g: 150, b: 150 }, + material: 'standard' + }, + clickable: true, + hoverable: true + }, + + // Storage tanks + { + id: 'tank_1', + type: 'cylinder', + name: 'Storage Tank 1', + transform: { + position: { x: -10, y: 3, z: -8 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 } + }, + geometry: { + radius: 2, + height: 6, + segments: 16 + }, + style: { + color: { r: 200, g: 50, b: 50 }, + material: 'standard' + }, + liveData: [{ + variableName: 'Tank1Level', + property: 'style.color.g', + transform: { + type: 'linear', + inputRange: [0, 100], + outputRange: [50, 200] + } + }], + clickable: true, + hoverable: true + } + ], + + connections: [], + + liveDataConfig: { + enabled: true, + updateInterval: 500, + variables: ['Tank1Level', 'Tank2Level', 'ConveyorSpeed', 'Temperature'] + }, + + ui: { + showViewModeToggle: true, + showControls: true, + showStats: true, + showGrid: true, + showAxes: true + }, + + performance: { + shadows: true, + antialias: true, + pixelRatio: Math.min(window.devicePixelRatio, 2) + } +}; + +// Simple Demo Preset +export const simpleDemo: IsometricDiagramConfig = { + id: 'simple-demo', + name: 'Simple Demo', + version: '1.0.0', + + scene: { + background: { r: 240, g: 240, b: 240 }, + environment: 'studio', + grid: { + enabled: true, + size: 10, + divisions: 10, + color: { r: 180, g: 180, b: 180 } + } + }, + + camera: { + position: { x: 8, y: 8, z: 8 }, + target: { x: 0, y: 0, z: 0 }, + fov: 45, + near: 0.1, + far: 100, + enableControls: true + }, + + viewModes: { + default: 'isometric', + presets: [ + { + name: 'Isometric', + camera: { + position: { x: 8, y: 8, z: 8 }, + target: { x: 0, y: 0, z: 0 } + } + }, + { + name: '2D Top', + camera: { + position: { x: 0, y: 15, z: 0 }, + target: { x: 0, y: 0, z: 0 } + }, + is2D: true + } + ] + }, + + nodes: [ + { + id: 'demo_box', + type: 'box', + name: 'Demo Box', + transform: { + position: { x: 0, y: 1, z: 0 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 } + }, + geometry: { + width: 2, + height: 2, + depth: 2 + }, + style: { + color: { r: 100, g: 150, b: 200 }, + material: 'standard' + }, + animations: [{ + property: 'transform.rotation.y', + keyframes: [ + { time: 0, value: 0 }, + { time: 1, value: Math.PI * 2 } + ], + duration: 4, + loop: true, + autoplay: true + }], + clickable: true, + hoverable: true + }, + + { + id: 'demo_sphere', + type: 'sphere', + name: 'Demo Sphere', + transform: { + position: { x: 3, y: 0.5, z: 0 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 } + }, + geometry: { + radius: 0.5, + segments: 16 + }, + style: { + color: { r: 200, g: 100, b: 100 }, + material: 'standard' + }, + clickable: true, + hoverable: true + } + ], + + connections: [], + + ui: { + showViewModeToggle: true, + showControls: true, + showStats: false, + showGrid: true, + showAxes: false + }, + + performance: { + shadows: false, + antialias: true, + pixelRatio: 1 + } +}; + +// Export all presets +export const presets = { + trafficLightIntersection, + industrialPlant, + simpleDemo +}; + +export default presets; \ No newline at end of file diff --git a/apps/ui/app/components/isometric/types.ts b/apps/ui/app/components/isometric/types.ts new file mode 100644 index 0000000..19b1901 --- /dev/null +++ b/apps/ui/app/components/isometric/types.ts @@ -0,0 +1,270 @@ +export interface Vector3 { + x: number; + y: number; + z: number; +} + +export interface Vector2 { + x: number; + y: number; +} + +export interface Color { + r: number; + g: number; + b: number; + a?: number; +} + +export interface Transform { + position: Vector3; + rotation: Vector3; + scale: Vector3; +} + +export interface NodeStyle { + color?: Color; + borderColor?: Color; + borderWidth?: number; + opacity?: number; + material?: 'standard' | 'basic' | 'lambert' | 'phong'; + wireframe?: boolean; + visible?: boolean; +} + +export interface AnimationKeyframe { + time: number; + value: any; + easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'; +} + +export interface NodeAnimation { + property: string; // e.g., 'position.y', 'style.color.r', 'style.opacity' + keyframes: AnimationKeyframe[]; + duration?: number; + loop?: boolean; + autoplay?: boolean; +} + +export interface LiveDataBinding { + variableName: string; // PLC variable name from WebSocket + property: string; // Node property to update (e.g., 'position.y', 'style.color.r') + transform?: { + type: 'linear' | 'step' | 'custom'; + inputRange?: [number, number]; + outputRange?: [number, number]; + steps?: { input: number; output: any }[]; + customFunction?: string; // JavaScript function as string + }; +} + +export interface IsometricNode { + id: string; + type: 'box' | 'cylinder' | 'sphere' | 'cone' | 'plane' | 'group' | 'svg' | 'gltf' | 'custom'; + name?: string; + transform: Transform; + style?: NodeStyle; + + // Geometry-specific properties + geometry?: { + width?: number; + height?: number; + depth?: number; + radius?: number; + segments?: number; + detail?: number; + }; + + // Content for different node types + content?: { + svgPath?: string; // For SVG nodes + gltfUrl?: string; // For 3D model nodes + text?: string; // For text nodes + htmlContent?: string; // For HTML overlay nodes + }; + + // Children nodes (for groups) + children?: IsometricNode[]; + + // Interactivity + interactive?: boolean; + clickable?: boolean; + hoverable?: boolean; + + // Animation + animations?: NodeAnimation[]; + + // Live data integration + liveData?: LiveDataBinding[]; + + // Custom properties for user extensions + userData?: Record; +} + +export interface Connection { + id: string; + fromNodeId: string; + toNodeId: string; + fromPoint?: Vector3; // Connection point relative to fromNode + toPoint?: Vector3; // Connection point relative to toNode + type: 'line' | 'curve' | 'pipe' | 'wire' | 'arrow'; + style?: { + color?: Color; + width?: number; + dashPattern?: number[]; + arrowSize?: number; + curveHeight?: number; + }; + animated?: boolean; + animationSpeed?: number; + liveData?: LiveDataBinding[]; +} + +export interface ViewPreset { + name: string; + camera: { + position: Vector3; + target: Vector3; + fov?: number; + }; + is2D?: boolean; +} + +export interface IsometricDiagramConfig { + id: string; + name: string; + version: string; + + // Scene configuration + scene: { + background?: Color; + environment?: 'studio' | 'sunset' | 'dawn' | 'night' | 'warehouse' | 'apartment' | 'city' | 'park' | 'custom'; + environmentUrl?: string; // For custom environment + fog?: { + enabled: boolean; + color?: Color; + near?: number; + far?: number; + }; + grid?: { + enabled: boolean; + size?: number; + divisions?: number; + color?: Color; + }; + }; + + // Camera settings + camera: { + position: Vector3; + target: Vector3; + fov: number; + near: number; + far: number; + enableControls: boolean; + constrainPan?: boolean; + constrainZoom?: boolean; + minDistance?: number; + maxDistance?: number; + }; + + // View modes + viewModes: { + default: '2d' | '3d' | 'isometric'; + presets: ViewPreset[]; + }; + + // Nodes and connections + nodes: IsometricNode[]; + connections: Connection[]; + + // Live data configuration + liveDataConfig?: { + enabled: boolean; + updateInterval?: number; // milliseconds + websocketUrl?: string; + variables?: string[]; // List of PLC variables to subscribe to + }; + + // User interface + ui?: { + showViewModeToggle?: boolean; + showControls?: boolean; + showStats?: boolean; + showGrid?: boolean; + showAxes?: boolean; + customControls?: Array<{ + id: string; + type: 'button' | 'slider' | 'toggle' | 'select'; + label: string; + action: string; // Function name or inline JS + position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; + }>; + }; + + // Performance settings + performance?: { + shadows?: boolean; + antialias?: boolean; + pixelRatio?: number; + maxFramerate?: number; + }; +} + +// API types for runtime data integration +export interface LiveDataValue { + variableName: string; + value: any; + timestamp: number; + quality?: 'good' | 'bad' | 'uncertain'; +} + +export interface LiveDataUpdate { + controllerName: string; + variables: LiveDataValue[]; +} + +export interface IsometricAPI { + // Node manipulation + updateNode(nodeId: string, updates: Partial): void; + addNode(node: IsometricNode, parentId?: string): void; + removeNode(nodeId: string): void; + getNode(nodeId: string): IsometricNode | null; + + // Animation control + playAnimation(nodeId: string, animationIndex?: number): void; + pauseAnimation(nodeId: string, animationIndex?: number): void; + stopAnimation(nodeId: string, animationIndex?: number): void; + + // View control + setViewMode(mode: '2d' | '3d' | 'isometric'): void; + setViewPreset(presetName: string): void; + setCameraPosition(position: Vector3, target?: Vector3): void; + + // Live data integration + updateLiveData(data: LiveDataUpdate): void; + subscribeTo(variableNames: string[]): void; + unsubscribeFrom(variableNames: string[]): void; + + // Event handling + onNodeClick(callback: (nodeId: string, event: any) => void): void; + onNodeHover(callback: (nodeId: string, isHovering: boolean, event: any) => void): void; + onViewModeChange(callback: (mode: '2d' | '3d' | 'isometric') => void): void; + + // Utility + exportToImage(options?: { width?: number; height?: number; format?: 'png' | 'jpg' }): Promise; + exportToGLTF(): Promise; + reset(): void; +} + +// Component props +export interface IsometricProps { + config: IsometricDiagramConfig; + plcVariables?: Record; + onApiReady?: (api: IsometricAPI) => void; + onNodeClick?: (nodeId: string, event: any) => void; + onNodeHover?: (nodeId: string, isHovering: boolean, event: any) => void; + onViewModeChange?: (mode: '2d' | '3d' | 'isometric') => void; + className?: string; + style?: React.CSSProperties; +} \ No newline at end of file diff --git a/apps/ui/app/components/isometric/utils.ts b/apps/ui/app/components/isometric/utils.ts new file mode 100644 index 0000000..d3d6bc7 --- /dev/null +++ b/apps/ui/app/components/isometric/utils.ts @@ -0,0 +1,301 @@ +import { IsometricNode, IsometricDiagramConfig, Vector3, Color, Transform } from './types'; + +// Utility functions for creating common node types +export const createBox = ( + id: string, + position: Vector3, + size: { width: number; height: number; depth: number }, + color?: Color +): IsometricNode => ({ + id, + type: 'box', + transform: { + position, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 } + }, + geometry: size, + style: color ? { color } : undefined, + clickable: true, + hoverable: true +}); + +export const createCylinder = ( + id: string, + position: Vector3, + size: { radius: number; height: number }, + color?: Color +): IsometricNode => ({ + id, + type: 'cylinder', + transform: { + position, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 } + }, + geometry: size, + style: color ? { color } : undefined, + clickable: true, + hoverable: true +}); + +export const createSphere = ( + id: string, + position: Vector3, + radius: number, + color?: Color +): IsometricNode => ({ + id, + type: 'sphere', + transform: { + position, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 } + }, + geometry: { radius }, + style: color ? { color } : undefined, + clickable: true, + hoverable: true +}); + +export const createTrafficLight = ( + id: string, + position: Vector3, + variablePrefix: string = '' +): IsometricNode => { + const poleHeight = 6; + const lightSize = 0.8; + const spacing = 1.2; + + return { + id, + type: 'group', + name: 'Traffic Light', + transform: { + position, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 } + }, + children: [ + // Pole + createCylinder( + `${id}_pole`, + { x: 0, y: poleHeight / 2, z: 0 }, + { radius: 0.1, height: poleHeight }, + { r: 80, g: 80, b: 80 } + ), + + // Housing + createBox( + `${id}_housing`, + { x: 0, y: poleHeight + 2, z: 0 }, + { width: 1, height: 4, depth: 0.3 }, + { r: 60, g: 60, b: 60 } + ), + + // Red Light + { + id: `${id}_red`, + type: 'sphere', + name: 'Red Light', + transform: { + position: { x: 0, y: poleHeight + 3, z: 0.2 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 } + }, + geometry: { radius: lightSize / 2 }, + style: { + color: { r: 100, g: 0, b: 0 }, + material: 'standard' + }, + liveData: [{ + variableName: `${variablePrefix}MainRoadRed`, + property: 'style.color', + transform: { + type: 'step', + steps: [ + { input: 0, output: { r: 100, g: 0, b: 0 } }, + { input: 1, output: { r: 255, g: 0, b: 0 } } + ] + } + }], + clickable: true, + hoverable: true + }, + + // Yellow Light + { + id: `${id}_yellow`, + type: 'sphere', + name: 'Yellow Light', + transform: { + position: { x: 0, y: poleHeight + 2, z: 0.2 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 } + }, + geometry: { radius: lightSize / 2 }, + style: { + color: { r: 100, g: 100, b: 0 }, + material: 'standard' + }, + liveData: [{ + variableName: `${variablePrefix}MainRoadYellow`, + property: 'style.color', + transform: { + type: 'step', + steps: [ + { input: 0, output: { r: 100, g: 100, b: 0 } }, + { input: 1, output: { r: 255, g: 255, b: 0 } } + ] + } + }], + clickable: true, + hoverable: true + }, + + // Green Light + { + id: `${id}_green`, + type: 'sphere', + name: 'Green Light', + transform: { + position: { x: 0, y: poleHeight + 1, z: 0.2 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 } + }, + geometry: { radius: lightSize / 2 }, + style: { + color: { r: 0, g: 100, b: 0 }, + material: 'standard' + }, + liveData: [{ + variableName: `${variablePrefix}MainRoadGreen`, + property: 'style.color', + transform: { + type: 'step', + steps: [ + { input: 0, output: { r: 0, g: 100, b: 0 } }, + { input: 1, output: { r: 0, g: 255, b: 0 } } + ] + } + }], + clickable: true, + hoverable: true + } + ], + clickable: true, + hoverable: true + }; +}; + +export const createIntersection = (): IsometricNode[] => { + const roadWidth = 8; + const roadLength = 20; + + return [ + // Main road (horizontal) + createBox( + 'main_road', + { x: 0, y: 0, z: 0 }, + { width: roadLength, height: 0.1, depth: roadWidth }, + { r: 80, g: 80, b: 80 } + ), + + // Side road (vertical) + createBox( + 'side_road', + { x: 0, y: 0, z: 0 }, + { width: roadWidth, height: 0.1, depth: roadLength }, + { r: 80, g: 80, b: 80 } + ), + + // Traffic lights + createTrafficLight('traffic_light_1', { x: -4, y: 0, z: -4 }), + createTrafficLight('traffic_light_2', { x: 4, y: 0, z: 4 }), + + // Road markings + createBox( + 'center_line_h', + { x: 0, y: 0.11, z: 0 }, + { width: roadLength, height: 0.01, depth: 0.2 }, + { r: 255, g: 255, b: 255 } + ), + createBox( + 'center_line_v', + { x: 0, y: 0.11, z: 0 }, + { width: 0.2, height: 0.01, depth: roadLength }, + { r: 255, g: 255, b: 255 } + ) + ]; +}; + +// Color utilities +export const Colors = { + RED: { r: 255, g: 0, b: 0 }, + GREEN: { r: 0, g: 255, b: 0 }, + BLUE: { r: 0, g: 0, b: 255 }, + YELLOW: { r: 255, g: 255, b: 0 }, + WHITE: { r: 255, g: 255, b: 255 }, + BLACK: { r: 0, g: 0, b: 0 }, + GRAY: { r: 128, g: 128, b: 128 }, + DARK_GRAY: { r: 80, g: 80, b: 80 }, + LIGHT_GRAY: { r: 200, g: 200, b: 200 } +}; + +// Position utilities +export const Positions = { + ORIGIN: { x: 0, y: 0, z: 0 }, + UP: (distance: number): Vector3 => ({ x: 0, y: distance, z: 0 }), + DOWN: (distance: number): Vector3 => ({ x: 0, y: -distance, z: 0 }), + LEFT: (distance: number): Vector3 => ({ x: -distance, y: 0, z: 0 }), + RIGHT: (distance: number): Vector3 => ({ x: distance, y: 0, z: 0 }), + FORWARD: (distance: number): Vector3 => ({ x: 0, y: 0, z: distance }), + BACK: (distance: number): Vector3 => ({ x: 0, y: 0, z: -distance }) +}; + +// Animation utilities +export const createPulseAnimation = (property: string, minValue: number, maxValue: number) => ({ + property, + keyframes: [ + { time: 0, value: minValue }, + { time: 0.5, value: maxValue }, + { time: 1, value: minValue } + ], + duration: 2, + loop: true, + autoplay: true +}); + +export const createRotationAnimation = (axis: 'x' | 'y' | 'z', speed: number = 1) => ({ + property: `transform.rotation.${axis}`, + keyframes: [ + { time: 0, value: 0 }, + { time: 1, value: Math.PI * 2 } + ], + duration: 2 / speed, + loop: true, + autoplay: true +}); + +// Live data binding utilities +export const createColorBinding = (variableName: string, trueColor: Color, falseColor: Color) => ({ + variableName, + property: 'style.color', + transform: { + type: 'step' as const, + steps: [ + { input: false, output: falseColor }, + { input: true, output: trueColor } + ] + } +}); + +export const createPositionBinding = (variableName: string, axis: 'x' | 'y' | 'z', inputRange: [number, number], outputRange: [number, number]) => ({ + variableName, + property: `transform.position.${axis}`, + transform: { + type: 'linear' as const, + inputRange, + outputRange + } +}); \ No newline at end of file diff --git a/apps/ui/app/hooks/useESBuildWorker.ts b/apps/ui/app/hooks/useESBuildWorker.ts index 860bc4d..cd6409e 100644 --- a/apps/ui/app/hooks/useESBuildWorker.ts +++ b/apps/ui/app/hooks/useESBuildWorker.ts @@ -76,36 +76,110 @@ export function useESBuildWorker(): UseESBuildWorkerResult { Component Preview + + + @@ -114,13 +188,39 @@ export function useESBuildWorker(): UseESBuildWorkerResult { // Make React and ReactDOM globally available window.React = React; window.ReactDOM = ReactDOM; + + // Make Three.js and R3F libraries globally available + window.THREE = THREE; + if (window.ReactThreeFiber) { + window.Canvas = window.ReactThreeFiber.Canvas; + window.useFrame = window.ReactThreeFiber.useFrame; + window.useThree = window.ReactThreeFiber.useThree; + } + if (window.ReactThreeDrei) { + window.OrbitControls = window.ReactThreeDrei.OrbitControls; + window.Environment = window.ReactThreeDrei.Environment; + window.Grid = window.ReactThreeDrei.Grid; + window.Stats = window.ReactThreeDrei.Stats; + window.Box = window.ReactThreeDrei.Box; + window.Cylinder = window.ReactThreeDrei.Cylinder; + window.Sphere = window.ReactThreeDrei.Sphere; + window.Cone = window.ReactThreeDrei.Cone; + window.Plane = window.ReactThreeDrei.Plane; + } // Create a function to render the component function renderComponent(Component) { if (Component) { ReactDOM.createRoot(document.getElementById('root')).render( React.createElement(Component, { - plcVariables: { RedLight: true, YellowLight: false, GreenLight: false } + plcVariables: { + MainRoadRed: true, + MainRoadYellow: false, + MainRoadGreen: false, + SideRoadRed: false, + SideRoadYellow: false, + SideRoadGreen: true + } }) ); } else { @@ -146,6 +246,8 @@ export function useESBuildWorker(): UseESBuildWorkerResult { // Expose the component to the global scope window.ComponentToRender = typeof TrafficLights !== 'undefined' ? TrafficLights : + typeof IsometricTrafficControl !== 'undefined' ? IsometricTrafficControl : + typeof SimpleIsometricDemo !== 'undefined' ? SimpleIsometricDemo : typeof default_1 !== 'undefined' ? default_1 : window.default || window.Component; diff --git a/apps/ui/examples/example-1/control/IsometricTrafficControl.tsx b/apps/ui/examples/example-1/control/IsometricTrafficControl.tsx new file mode 100644 index 0000000..7e14144 --- /dev/null +++ b/apps/ui/examples/example-1/control/IsometricTrafficControl.tsx @@ -0,0 +1,251 @@ +import React, { useEffect, useState } from 'react'; +import { Isometric, IsometricAPI, trafficLightIntersection } from '../../../app/components/isometric'; + +interface IsometricTrafficControlProps { + plcVariables?: Record; +} + +export default function IsometricTrafficControl({ + plcVariables = {}, +}: IsometricTrafficControlProps) { + const [api, setApi] = useState(null); + const [selectedNode, setSelectedNode] = useState(null); + const [hoveredNode, setHoveredNode] = useState(null); + const [viewMode, setViewMode] = useState<'2d' | '3d' | 'isometric'>('isometric'); + + // Handle API ready + const handleApiReady = (isometricApi: IsometricAPI) => { + setApi(isometricApi); + console.log('Isometric API ready:', isometricApi); + }; + + // Handle node clicks + const handleNodeClick = (nodeId: string, event: any) => { + setSelectedNode(nodeId); + console.log('Node clicked:', nodeId, event); + }; + + // Handle node hover + const handleNodeHover = (nodeId: string, isHovering: boolean, event: any) => { + setHoveredNode(isHovering ? nodeId : null); + console.log('Node hover:', nodeId, isHovering); + }; + + // Handle view mode changes + const handleViewModeChange = (mode: '2d' | '3d' | 'isometric') => { + setViewMode(mode); + console.log('View mode changed:', mode); + }; + + // Export functionality + const handleExportImage = async () => { + if (api) { + try { + const imageData = await api.exportToImage({ format: 'png', width: 1920, height: 1080 }); + const link = document.createElement('a'); + link.download = 'traffic-intersection.png'; + link.href = imageData; + link.click(); + } catch (error) { + console.error('Failed to export image:', error); + } + } + }; + + // Reset camera + const handleResetCamera = () => { + if (api) { + api.setCameraPosition( + { x: 15, y: 15, z: 15 }, + { x: 0, y: 0, z: 0 } + ); + } + }; + + // Update traffic light colors based on PLC variables + useEffect(() => { + if (api && plcVariables) { + // Update main road traffic light colors + if (plcVariables.MainRoadRed !== undefined) { + api.updateNode('traffic_light_1_red', { + style: { + color: plcVariables.MainRoadRed + ? { r: 255, g: 0, b: 0 } + : { r: 100, g: 0, b: 0 } + } + }); + } + + if (plcVariables.MainRoadYellow !== undefined) { + api.updateNode('traffic_light_1_yellow', { + style: { + color: plcVariables.MainRoadYellow + ? { r: 255, g: 255, b: 0 } + : { r: 100, g: 100, b: 0 } + } + }); + } + + if (plcVariables.MainRoadGreen !== undefined) { + api.updateNode('traffic_light_1_green', { + style: { + color: plcVariables.MainRoadGreen + ? { r: 0, g: 255, b: 0 } + : { r: 0, g: 100, b: 0 } + } + }); + } + + // Update side road traffic light colors + if (plcVariables.SideRoadRed !== undefined) { + api.updateNode('traffic_light_2_red', { + style: { + color: plcVariables.SideRoadRed + ? { r: 255, g: 0, b: 0 } + : { r: 100, g: 0, b: 0 } + } + }); + } + + if (plcVariables.SideRoadYellow !== undefined) { + api.updateNode('traffic_light_2_yellow', { + style: { + color: plcVariables.SideRoadYellow + ? { r: 255, g: 255, b: 0 } + : { r: 100, g: 100, b: 0 } + } + }); + } + + if (plcVariables.SideRoadGreen !== undefined) { + api.updateNode('traffic_light_2_green', { + style: { + color: plcVariables.SideRoadGreen + ? { r: 0, g: 255, b: 0 } + : { r: 0, g: 100, b: 0 } + } + }); + } + } + }, [api, plcVariables]); + + return ( +
+ {/* Header */} +
+
+
+

Traffic Light Intersection

+

+ 3D Isometric visualization of traffic control system +

+
+ +
+ + +
+
+
+ + {/* Status Panel */} +
+

Traffic Status

+ +
+
+ Main Road: +
+
+
+
+
+
+ +
+ Side Road: +
+
+
+
+
+
+
+ + {selectedNode && ( +
+

Selected:

+

{selectedNode}

+
+ )} + + {hoveredNode && ( +
+

Hovering:

+

{hoveredNode}

+
+ )} +
+ + {/* Debug Panel */} +
+

Debug Info

+
+
View Mode: {viewMode}
+
R3F Loaded: {api ? '✓' : '✗'}
+
Live Data: {Object.keys(plcVariables).length} variables
+
+ {JSON.stringify(plcVariables, null, 2)} +
+
+
+ + {/* Main Isometric View */} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/apps/ui/examples/example-1/control/README.md b/apps/ui/examples/example-1/control/README.md new file mode 100644 index 0000000..ae03d20 --- /dev/null +++ b/apps/ui/examples/example-1/control/README.md @@ -0,0 +1,118 @@ +# Isometric Diagram System - Example 1 + +This folder contains examples of the new Isometric diagram system using React Three Fiber (R3F) and drei. + +## Components + +### 1. TrafficLights.tsx (Original) +The original 2D traffic light component showing basic React implementation. + +### 2. SimpleIsometricDemo.tsx +A basic 3D isometric demo showing: +- Simple 3D boxes representing traffic lights +- Live data integration with PLC variables +- Basic R3F Canvas setup +- Color changes based on `MainRoadRed` and `MainRoadGreen` variables + +### 3. IsometricTrafficControl.tsx +Advanced 3D isometric traffic intersection with: +- Full traffic intersection with roads and traffic lights +- Multiple view modes (2D, Isometric, 3D) +- Live data integration from PLC +- Interactive camera controls +- Export functionality +- Status and debug panels + +### 4. traffic-intersection.json +JSON configuration file defining the complete isometric diagram structure including: +- Scene configuration (background, environment, grid) +- Camera settings and view presets +- Node definitions (roads, traffic lights, etc.) +- Live data bindings +- UI configuration + +## Features + +### Live Data Integration +The isometric components automatically update based on PLC variables: +- `MainRoadRed`, `MainRoadYellow`, `MainRoadGreen` +- `SideRoadRed`, `SideRoadYellow`, `SideRoadGreen` + +### View Modes +- **2D**: Top-down orthographic view +- **Isometric**: Classic isometric perspective +- **3D**: Full 3D perspective with free camera movement + +### Interactive Features +- Camera controls (zoom, pan, rotate) +- Node hover and click detection +- Export to PNG functionality +- Real-time status display + +## How to Test + +1. Go to the Logic page and open the example-1 project +2. In the Control section, try previewing different components: + - `SimpleIsometricDemo.tsx` - Basic R3F test + - `IsometricTrafficControl.tsx` - Full isometric system + - `TrafficLights.tsx` - Original 2D version + +3. The components will show live data from the traffic light simulation +4. Use the view mode buttons to switch between 2D/Isometric/3D views +5. Camera can be controlled with mouse (drag to rotate, scroll to zoom) + +## API Usage + +```typescript +import { Isometric, trafficLightIntersection } from '../../../app/components/isometric'; + + setApi(api)} + onNodeClick={(nodeId, event) => console.log('Clicked:', nodeId)} + onNodeHover={(nodeId, isHovering) => console.log('Hover:', nodeId)} + onViewModeChange={(mode) => console.log('View mode:', mode)} +/> +``` + +## Dependencies + +The system uses these external libraries (loaded via CDN in preview): +- Three.js (3D engine) +- React Three Fiber (React bindings for Three.js) +- Drei (Helper components for R3F) + +## JSON Configuration + +Diagrams can be defined declaratively using JSON: + +```json +{ + "nodes": [ + { + "id": "traffic_light_1", + "type": "group", + "children": [ + { + "id": "red_light", + "type": "sphere", + "liveData": [{ + "variableName": "MainRoadRed", + "property": "style.color", + "transform": { + "type": "step", + "steps": [ + { "input": 0, "output": { "r": 100, "g": 0, "b": 0 } }, + { "input": 1, "output": { "r": 255, "g": 0, "b": 0 } } + ] + } + }] + } + ] + } + ] +} +``` + +This enables users to create complex 3D visualizations that respond to live data from their control systems. \ No newline at end of file diff --git a/apps/ui/examples/example-1/control/SimpleIsometricDemo.tsx b/apps/ui/examples/example-1/control/SimpleIsometricDemo.tsx new file mode 100644 index 0000000..405419f --- /dev/null +++ b/apps/ui/examples/example-1/control/SimpleIsometricDemo.tsx @@ -0,0 +1,121 @@ +import React from 'react'; + +interface SimpleIsometricDemoProps { + plcVariables?: Record; +} + +export default function SimpleIsometricDemo({ + plcVariables = {}, +}: SimpleIsometricDemoProps) { + // Check if R3F libraries are available + const Canvas = (window as any).Canvas; + const Box = (window as any).Box; + const OrbitControls = (window as any).OrbitControls; + + if (!Canvas || !Box) { + return ( +
+
+

R3F Loading...

+

+ React Three Fiber libraries are loading. Please wait... +

+
+

+ Available: {Object.keys(window as any).filter(k => k.includes('Three') || k.includes('React')).join(', ')} +

+
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+

Simple Isometric Demo

+

+ Basic Three.js + React Three Fiber test +

+
+ + {/* Status Panel */} +
+

Status

+
+
+ Red Light: +
+
+
+ Green Light: +
+
+
+
+ + {/* Debug Panel */} +
+

Debug

+
+
Canvas: {Canvas ? '✓' : '✗'}
+
Box: {Box ? '✓' : '✗'}
+
OrbitControls: {OrbitControls ? '✓' : '✗'}
+
+ {JSON.stringify(plcVariables, null, 2)} +
+
+
+ + {/* 3D Scene */} +
+ + + + + {/* Traffic Light Red - changes color based on PLC variable */} + + + + + {/* Traffic Light Green */} + + + + + {/* Road */} + + + + + {/* Traffic Light Pole */} + + + + + {OrbitControls && } + +
+
+ ); +} \ No newline at end of file diff --git a/apps/ui/examples/example-1/control/traffic-intersection.json b/apps/ui/examples/example-1/control/traffic-intersection.json new file mode 100644 index 0000000..e5d41dc --- /dev/null +++ b/apps/ui/examples/example-1/control/traffic-intersection.json @@ -0,0 +1,479 @@ +{ + "id": "traffic-intersection-example-1", + "name": "Traffic Intersection - Example 1", + "version": "1.0.0", + "scene": { + "background": { "r": 135, "g": 206, "b": 235 }, + "environment": "city", + "fog": { + "enabled": false, + "color": { "r": 200, "g": 200, "b": 200 }, + "near": 10, + "far": 50 + }, + "grid": { + "enabled": true, + "size": 20, + "divisions": 20, + "color": { "r": 100, "g": 100, "b": 100 } + } + }, + "camera": { + "position": { "x": 15, "y": 15, "z": 15 }, + "target": { "x": 0, "y": 0, "z": 0 }, + "fov": 50, + "near": 0.1, + "far": 1000, + "enableControls": true, + "constrainPan": false, + "constrainZoom": false, + "minDistance": 5, + "maxDistance": 50 + }, + "viewModes": { + "default": "isometric", + "presets": [ + { + "name": "Isometric", + "camera": { + "position": { "x": 15, "y": 15, "z": 15 }, + "target": { "x": 0, "y": 0, "z": 0 } + } + }, + { + "name": "Top Down", + "camera": { + "position": { "x": 0, "y": 30, "z": 0 }, + "target": { "x": 0, "y": 0, "z": 0 } + }, + "is2D": true + }, + { + "name": "Side View", + "camera": { + "position": { "x": 30, "y": 5, "z": 0 }, + "target": { "x": 0, "y": 5, "z": 0 } + } + } + ] + }, + "nodes": [ + { + "id": "main_road", + "type": "box", + "name": "Main Road", + "transform": { + "position": { "x": 0, "y": 0, "z": 0 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "geometry": { + "width": 20, + "height": 0.1, + "depth": 8 + }, + "style": { + "color": { "r": 80, "g": 80, "b": 80 }, + "material": "standard" + }, + "clickable": true, + "hoverable": true + }, + { + "id": "side_road", + "type": "box", + "name": "Side Road", + "transform": { + "position": { "x": 0, "y": 0, "z": 0 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "geometry": { + "width": 8, + "height": 0.1, + "depth": 20 + }, + "style": { + "color": { "r": 80, "g": 80, "b": 80 }, + "material": "standard" + }, + "clickable": true, + "hoverable": true + }, + { + "id": "traffic_light_1", + "type": "group", + "name": "Main Traffic Light", + "transform": { + "position": { "x": -4, "y": 0, "z": -4 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "children": [ + { + "id": "traffic_light_1_pole", + "type": "cylinder", + "name": "Traffic Light Pole", + "transform": { + "position": { "x": 0, "y": 3, "z": 0 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "geometry": { + "radius": 0.1, + "height": 6, + "segments": 8 + }, + "style": { + "color": { "r": 80, "g": 80, "b": 80 }, + "material": "standard" + }, + "clickable": true, + "hoverable": true + }, + { + "id": "traffic_light_1_housing", + "type": "box", + "name": "Traffic Light Housing", + "transform": { + "position": { "x": 0, "y": 8, "z": 0 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "geometry": { + "width": 1, + "height": 4, + "depth": 0.3 + }, + "style": { + "color": { "r": 60, "g": 60, "b": 60 }, + "material": "standard" + }, + "clickable": true, + "hoverable": true + }, + { + "id": "traffic_light_1_red", + "type": "sphere", + "name": "Red Light", + "transform": { + "position": { "x": 0, "y": 9, "z": 0.2 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "geometry": { + "radius": 0.4, + "segments": 16 + }, + "style": { + "color": { "r": 100, "g": 0, "b": 0 }, + "material": "standard" + }, + "liveData": [{ + "variableName": "MainRoadRed", + "property": "style.color", + "transform": { + "type": "step", + "steps": [ + { "input": 0, "output": { "r": 100, "g": 0, "b": 0 } }, + { "input": 1, "output": { "r": 255, "g": 0, "b": 0 } } + ] + } + }], + "clickable": true, + "hoverable": true + }, + { + "id": "traffic_light_1_yellow", + "type": "sphere", + "name": "Yellow Light", + "transform": { + "position": { "x": 0, "y": 8, "z": 0.2 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "geometry": { + "radius": 0.4, + "segments": 16 + }, + "style": { + "color": { "r": 100, "g": 100, "b": 0 }, + "material": "standard" + }, + "liveData": [{ + "variableName": "MainRoadYellow", + "property": "style.color", + "transform": { + "type": "step", + "steps": [ + { "input": 0, "output": { "r": 100, "g": 100, "b": 0 } }, + { "input": 1, "output": { "r": 255, "g": 255, "b": 0 } } + ] + } + }], + "clickable": true, + "hoverable": true + }, + { + "id": "traffic_light_1_green", + "type": "sphere", + "name": "Green Light", + "transform": { + "position": { "x": 0, "y": 7, "z": 0.2 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "geometry": { + "radius": 0.4, + "segments": 16 + }, + "style": { + "color": { "r": 0, "g": 100, "b": 0 }, + "material": "standard" + }, + "liveData": [{ + "variableName": "MainRoadGreen", + "property": "style.color", + "transform": { + "type": "step", + "steps": [ + { "input": 0, "output": { "r": 0, "g": 100, "b": 0 } }, + { "input": 1, "output": { "r": 0, "g": 255, "b": 0 } } + ] + } + }], + "clickable": true, + "hoverable": true + } + ], + "clickable": true, + "hoverable": true + }, + { + "id": "traffic_light_2", + "type": "group", + "name": "Side Traffic Light", + "transform": { + "position": { "x": 4, "y": 0, "z": 4 }, + "rotation": { "x": 0, "y": 3.14159, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "children": [ + { + "id": "traffic_light_2_pole", + "type": "cylinder", + "name": "Traffic Light Pole", + "transform": { + "position": { "x": 0, "y": 3, "z": 0 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "geometry": { + "radius": 0.1, + "height": 6, + "segments": 8 + }, + "style": { + "color": { "r": 80, "g": 80, "b": 80 }, + "material": "standard" + }, + "clickable": true, + "hoverable": true + }, + { + "id": "traffic_light_2_housing", + "type": "box", + "name": "Traffic Light Housing", + "transform": { + "position": { "x": 0, "y": 8, "z": 0 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "geometry": { + "width": 1, + "height": 4, + "depth": 0.3 + }, + "style": { + "color": { "r": 60, "g": 60, "b": 60 }, + "material": "standard" + }, + "clickable": true, + "hoverable": true + }, + { + "id": "traffic_light_2_red", + "type": "sphere", + "name": "Red Light", + "transform": { + "position": { "x": 0, "y": 9, "z": 0.2 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "geometry": { + "radius": 0.4, + "segments": 16 + }, + "style": { + "color": { "r": 100, "g": 0, "b": 0 }, + "material": "standard" + }, + "liveData": [{ + "variableName": "SideRoadRed", + "property": "style.color", + "transform": { + "type": "step", + "steps": [ + { "input": 0, "output": { "r": 100, "g": 0, "b": 0 } }, + { "input": 1, "output": { "r": 255, "g": 0, "b": 0 } } + ] + } + }], + "clickable": true, + "hoverable": true + }, + { + "id": "traffic_light_2_yellow", + "type": "sphere", + "name": "Yellow Light", + "transform": { + "position": { "x": 0, "y": 8, "z": 0.2 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "geometry": { + "radius": 0.4, + "segments": 16 + }, + "style": { + "color": { "r": 100, "g": 100, "b": 0 }, + "material": "standard" + }, + "liveData": [{ + "variableName": "SideRoadYellow", + "property": "style.color", + "transform": { + "type": "step", + "steps": [ + { "input": 0, "output": { "r": 100, "g": 100, "b": 0 } }, + { "input": 1, "output": { "r": 255, "g": 255, "b": 0 } } + ] + } + }], + "clickable": true, + "hoverable": true + }, + { + "id": "traffic_light_2_green", + "type": "sphere", + "name": "Green Light", + "transform": { + "position": { "x": 0, "y": 7, "z": 0.2 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "geometry": { + "radius": 0.4, + "segments": 16 + }, + "style": { + "color": { "r": 0, "g": 100, "b": 0 }, + "material": "standard" + }, + "liveData": [{ + "variableName": "SideRoadGreen", + "property": "style.color", + "transform": { + "type": "step", + "steps": [ + { "input": 0, "output": { "r": 0, "g": 100, "b": 0 } }, + { "input": 1, "output": { "r": 0, "g": 255, "b": 0 } } + ] + } + }], + "clickable": true, + "hoverable": true + } + ], + "clickable": true, + "hoverable": true + }, + { + "id": "center_line_h", + "type": "box", + "name": "Horizontal Center Line", + "transform": { + "position": { "x": 0, "y": 0.11, "z": 0 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "geometry": { + "width": 20, + "height": 0.01, + "depth": 0.2 + }, + "style": { + "color": { "r": 255, "g": 255, "b": 255 }, + "material": "standard" + }, + "clickable": false, + "hoverable": false + }, + { + "id": "center_line_v", + "type": "box", + "name": "Vertical Center Line", + "transform": { + "position": { "x": 0, "y": 0.11, "z": 0 }, + "rotation": { "x": 0, "y": 0, "z": 0 }, + "scale": { "x": 1, "y": 1, "z": 1 } + }, + "geometry": { + "width": 0.2, + "height": 0.01, + "depth": 20 + }, + "style": { + "color": { "r": 255, "g": 255, "b": 255 }, + "material": "standard" + }, + "clickable": false, + "hoverable": false + } + ], + "connections": [], + "liveDataConfig": { + "enabled": true, + "updateInterval": 100, + "variables": [ + "MainRoadRed", + "MainRoadYellow", + "MainRoadGreen", + "SideRoadRed", + "SideRoadYellow", + "SideRoadGreen" + ] + }, + "ui": { + "showViewModeToggle": true, + "showControls": true, + "showStats": false, + "showGrid": true, + "showAxes": false, + "customControls": [ + { + "id": "reset-camera", + "type": "button", + "label": "Reset Camera", + "action": "resetCamera", + "position": "top-left" + } + ] + }, + "performance": { + "shadows": true, + "antialias": true, + "pixelRatio": 2, + "maxFramerate": 60 + } +} \ No newline at end of file diff --git a/apps/ui/package.json b/apps/ui/package.json index e1c9ddf..1d7597a 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -28,6 +28,8 @@ "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-toast": "^1.2.14", "@radix-ui/react-tooltip": "^1.2.7", + "@react-three/drei": "^10.4.4", + "@react-three/fiber": "^9.2.0", "@tailwindcss/postcss": "^4.1.11", "@tanstack/react-query": "^5.81.5", "@tanstack/react-query-devtools": "^5.81.5", @@ -37,6 +39,7 @@ "@trpc/client": "11.4.3", "@trpc/react-query": "11.4.3", "@trpc/server": "11.4.3", + "@types/three": "^0.178.0", "@vitejs/plugin-react": "^4.6.0", "chevrotain": "^11.0.3", "chevrotain-allstar": "^0.3.1", @@ -58,6 +61,7 @@ "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11", "tailwindcss-animate": "^1.0.7", + "three": "^0.178.0", "vite-tsconfig-paths": "^5.1.4", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.12", diff --git a/apps/ui/pnpm-lock.yaml b/apps/ui/pnpm-lock.yaml index 92838f6..be2e3bf 100644 --- a/apps/ui/pnpm-lock.yaml +++ b/apps/ui/pnpm-lock.yaml @@ -50,6 +50,12 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.7 version: 1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-three/drei': + specifier: ^10.4.4 + version: 10.4.4(@react-three/fiber@9.2.0(@types/react@19.1.8)(immer@10.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(three@0.178.0))(@types/react@19.1.8)(@types/three@0.178.0)(immer@10.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(three@0.178.0) + '@react-three/fiber': + specifier: ^9.2.0 + version: 9.2.0(@types/react@19.1.8)(immer@10.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(three@0.178.0) '@tailwindcss/postcss': specifier: ^4.1.11 version: 4.1.11 @@ -77,6 +83,9 @@ importers: '@trpc/server': specifier: 11.4.3 version: 11.4.3(typescript@5.8.3) + '@types/three': + specifier: ^0.178.0 + version: 0.178.0 '@vitejs/plugin-react': specifier: ^4.6.0 version: 4.6.0(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) @@ -140,6 +149,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@4.1.11) + three: + specifier: ^0.178.0 + version: 0.178.0 vite-tsconfig-paths: specifier: ^5.1.4 version: 5.1.4(typescript@5.8.3)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) @@ -321,6 +333,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.27.6': + resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -627,6 +643,9 @@ packages: resolution: {integrity: sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==} engines: {node: '>=18'} + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@esbuild/aix-ppc64@0.25.5': resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} engines: {node: '>=18'} @@ -1120,6 +1139,9 @@ packages: '@material/typography@14.0.0-canary.53b3cad2f.0': resolution: {integrity: sha512-9J0k2fq7uyHsRzRqJDJLGmg3YzRpfRPtFDVeUH/xBcYoqpZE7wYw5Mb7s/l8eP626EtR7HhXhSPjvRTLA6NIJg==} + '@mediapipe/tasks-vision@0.10.17': + resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} + '@monaco-editor/loader@1.5.0': resolution: {integrity: sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==} @@ -1130,6 +1152,11 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@monogrid/gainmap-js@3.1.0': + resolution: {integrity: sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==} + peerDependencies: + three: '>= 0.159.0' + '@netlify/binary-info@1.0.0': resolution: {integrity: sha512-4wMPu9iN3/HL97QblBsBay3E1etIciR84izI3U+4iALY+JHCrI+a2jO0qbAZ/nxKoegypYEaiiqWXylm+/zfrw==} @@ -1729,6 +1756,42 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@react-three/drei@10.4.4': + resolution: {integrity: sha512-IrWgRyBsVb8rQOLZaTGiNucGvyfgD9g0ieedawparMA7TpNa6vvIPzckh9wsfps4Uz37JweuAKNGHM88FnhA0A==} + peerDependencies: + '@react-three/fiber': ^9.0.0 + react: ^19 + react-dom: ^19 + three: '>=0.159' + peerDependenciesMeta: + react-dom: + optional: true + + '@react-three/fiber@9.2.0': + resolution: {integrity: sha512-esZe+E9T/aYEM4HlBkirr/yRE8qWTp9WUsLISyHHMCHKlJv85uc5N4wwKw+Ay0QeTSITw6T9Q3Svpu383Q+CSQ==} + peerDependencies: + expo: '>=43.0' + expo-asset: '>=8.4' + expo-file-system: '>=11.0' + expo-gl: '>=11.0' + react: ^19.0.0 + react-dom: ^19.0.0 + react-native: '>=0.78' + three: '>=0.156' + peerDependenciesMeta: + expo: + optional: true + expo-asset: + optional: true + expo-file-system: + optional: true + expo-gl: + optional: true + react-dom: + optional: true + react-native: + optional: true + '@reduxjs/toolkit@2.8.2': resolution: {integrity: sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==} peerDependencies: @@ -2213,6 +2276,9 @@ packages: peerDependencies: typescript: '>=5.7.2' + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@types/babel__code-frame@7.0.6': resolution: {integrity: sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==} @@ -2258,6 +2324,9 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/draco3d@1.4.10': + resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -2267,20 +2336,34 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/offscreencanvas@2019.7.3': + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + '@types/react-dom@19.1.6': resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} peerDependencies: '@types/react': ^19.0.0 + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react@19.1.8': resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + '@types/tern@0.23.9': resolution: {integrity: sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==} + '@types/three@0.178.0': + resolution: {integrity: sha512-1IpVbMKbEAAWjyn0VTdVcNvI1h1NlTv3CcnwMr3NNBv/gi3PL0/EsWROnXUEkXBxl94MH5bZvS8h0WnBRmR/pQ==} + '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} @@ -2290,6 +2373,9 @@ packages: '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/webxr@0.5.22': + resolution: {integrity: sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -2319,6 +2405,14 @@ packages: resolution: {integrity: sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + '@vercel/nft@0.29.4': resolution: {integrity: sha512-6lLqMNX3TuycBPABycx7A9F1bHQR7kiQln6abjFbPrf5C/05qHM9M5E4PeTE59c7z8g6vHnx1Ioihb2AQl7BTA==} engines: {node: '>=18'} @@ -2348,6 +2442,9 @@ packages: '@vue/shared@3.5.17': resolution: {integrity: sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==} + '@webgpu/types@0.1.64': + resolution: {integrity: sha512-84kRIAGV46LJTlJZWxShiOrNL30A+9KokD7RB3dRCIqODFjodS5tCD5yyiZ8kIReGVZSDfA3XkkwyyOIF6K62A==} + '@whatwg-node/disposablestack@0.0.6': resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} engines: {node: '>=18.0.0'} @@ -2458,6 +2555,9 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2516,6 +2616,11 @@ packages: callsite@1.0.0: resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} + camera-controls@2.10.1: + resolution: {integrity: sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==} + peerDependencies: + three: '>=0.126.1' + caniuse-lite@1.0.30001727: resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} @@ -2673,6 +2778,11 @@ packages: resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==} engines: {node: '>=18.0'} + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2798,6 +2908,9 @@ packages: destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-gpu@5.0.70: + resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} + detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} @@ -2881,6 +2994,9 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} + draco3d@1.5.7: + resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -3062,6 +3178,12 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + fflate@0.6.10: + resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -3165,6 +3287,9 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + glsl-noise@0.0.0: + resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + gonzales-pe@4.3.0: resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==} engines: {node: '>=0.6.0'} @@ -3204,6 +3329,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hls.js@1.6.7: + resolution: {integrity: sha512-QW2fnwDGKGc9DwQUGLbmMOz8G48UZK7PVNJPcOUql1b8jubKx4/eMHNP5mGqr6tYlJNDG1g10Lx2U/qPzL6zwQ==} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -3244,6 +3372,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@10.1.1: resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} @@ -3326,6 +3457,9 @@ packages: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -3370,6 +3504,11 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + its-fine@2.0.0: + resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==} + peerDependencies: + react: ^19.0.0 + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -3432,6 +3571,9 @@ packages: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-darwin-arm64@1.30.1: resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} engines: {node: '>= 12.0.0'} @@ -3560,6 +3702,12 @@ packages: resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} engines: {node: '>=12'} + maath@0.10.8: + resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} + peerDependencies: + '@types/three': '>=0.134.0' + three: '>=0.134.0' + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -3586,6 +3734,14 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + meshline@3.3.1: + resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} + peerDependencies: + three: '>=0.137' + + meshoptimizer@0.18.1: + resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} + micro-api-client@3.3.0: resolution: {integrity: sha512-y0y6CUB9RLVsy3kfgayU28746QrNMpSm9O/AYGNsBgOkJr/X/Jk0VLGoO8Ude7Bpa8adywzF+MzXNZRFRsNPhg==} @@ -3897,6 +4053,9 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + precinct@12.2.0: resolution: {integrity: sha512-NFBMuwIfaJ4SocE9YXPU/n4AcNSoFMVFjP72nvl3cx69j/ke61/hPOWFREVxLkFhhEGnA8ZuVfTqJBa+PK3b5w==} engines: {node: '>=18'} @@ -3918,6 +4077,9 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + promise-worker-transferable@1.0.4: + resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -3961,6 +4123,12 @@ packages: react-is@19.1.0: resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==} + react-reconciler@0.31.0: + resolution: {integrity: sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^19.0.0 + react-redux@9.2.0: resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} peerDependencies: @@ -4007,6 +4175,15 @@ packages: '@types/react': optional: true + react-use-measure@2.1.7: + resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + peerDependenciesMeta: + react-dom: + optional: true + react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} @@ -4079,6 +4256,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + require-package-name@2.0.1: resolution: {integrity: sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==} @@ -4142,6 +4323,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.25.0: + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -4274,6 +4458,15 @@ packages: state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + stats-gl@2.4.2: + resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==} + peerDependencies: + '@types/three': '*' + three: '*' + + stats.js@0.17.0: + resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -4333,6 +4526,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + suspend-react@0.1.3: + resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} + peerDependencies: + react: '>=17.0' + system-architecture@0.1.0: resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} engines: {node: '>=18'} @@ -4370,6 +4568,19 @@ packages: text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + three-mesh-bvh@0.8.3: + resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==} + peerDependencies: + three: '>= 0.159.0' + + three-stdlib@2.36.0: + resolution: {integrity: sha512-kv0Byb++AXztEGsULgMAs8U2jgUdz6HPpAB/wDJnLiLlaWQX2APHhiTJIN7rqW+Of0eRgcp7jn05U1BsCP3xBA==} + peerDependencies: + three: '>=0.128.0' + + three@0.178.0: + resolution: {integrity: sha512-ybFIB0+x8mz0wnZgSGy2MO/WCO6xZhQSZnmfytSPyNpM0sBafGRVhdaj+erYh5U+RhQOAg/eXqw5uVDiM2BjhQ==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -4412,6 +4623,19 @@ packages: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} + troika-three-text@0.52.4: + resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} + peerDependencies: + three: '>=0.125.0' + + troika-three-utils@0.52.4: + resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==} + peerDependencies: + three: '>=0.125.0' + + troika-worker-utils@0.52.0: + resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -4436,6 +4660,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel-rat@0.1.2: + resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} + type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} @@ -4611,6 +4838,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true @@ -4705,6 +4936,12 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webgl-constants@1.1.1: + resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==} + + webgl-sdf-generator@1.1.1: + resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -4794,6 +5031,39 @@ packages: zod@3.25.75: resolution: {integrity: sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + + zustand@5.0.6: + resolution: {integrity: sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -4982,6 +5252,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/runtime@7.27.6': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -5626,6 +5898,8 @@ snapshots: gonzales-pe: 4.3.0 node-source-walk: 7.0.1 + '@dimforge/rapier3d-compat@0.12.0': {} + '@esbuild/aix-ppc64@0.25.5': optional: true @@ -6157,6 +6431,8 @@ snapshots: '@material/theme': 14.0.0-canary.53b3cad2f.0 tslib: 2.8.1 + '@mediapipe/tasks-vision@0.10.17': {} + '@monaco-editor/loader@1.5.0': dependencies: state-local: 1.0.7 @@ -6168,6 +6444,11 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + '@monogrid/gainmap-js@3.1.0(three@0.178.0)': + dependencies: + promise-worker-transferable: 1.0.4 + three: 0.178.0 + '@netlify/binary-info@1.0.0': {} '@netlify/blobs@9.1.2': @@ -6822,6 +7103,61 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@react-three/drei@10.4.4(@react-three/fiber@9.2.0(@types/react@19.1.8)(immer@10.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(three@0.178.0))(@types/react@19.1.8)(@types/three@0.178.0)(immer@10.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(three@0.178.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mediapipe/tasks-vision': 0.10.17 + '@monogrid/gainmap-js': 3.1.0(three@0.178.0) + '@react-three/fiber': 9.2.0(@types/react@19.1.8)(immer@10.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(three@0.178.0) + '@use-gesture/react': 10.3.1(react@19.1.0) + camera-controls: 2.10.1(three@0.178.0) + cross-env: 7.0.3 + detect-gpu: 5.0.70 + glsl-noise: 0.0.0 + hls.js: 1.6.7 + maath: 0.10.8(@types/three@0.178.0)(three@0.178.0) + meshline: 3.3.1(three@0.178.0) + react: 19.1.0 + stats-gl: 2.4.2(@types/three@0.178.0)(three@0.178.0) + stats.js: 0.17.0 + suspend-react: 0.1.3(react@19.1.0) + three: 0.178.0 + three-mesh-bvh: 0.8.3(three@0.178.0) + three-stdlib: 2.36.0(three@0.178.0) + troika-three-text: 0.52.4(three@0.178.0) + tunnel-rat: 0.1.2(@types/react@19.1.8)(immer@10.1.1)(react@19.1.0) + use-sync-external-store: 1.5.0(react@19.1.0) + utility-types: 3.11.0 + zustand: 5.0.6(@types/react@19.1.8)(immer@10.1.1)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + optionalDependencies: + react-dom: 19.1.0(react@19.1.0) + transitivePeerDependencies: + - '@types/react' + - '@types/three' + - immer + + '@react-three/fiber@9.2.0(@types/react@19.1.8)(immer@10.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(three@0.178.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@types/react-reconciler': 0.28.9(@types/react@19.1.8) + '@types/webxr': 0.5.22 + base64-js: 1.5.1 + buffer: 6.0.3 + its-fine: 2.0.0(@types/react@19.1.8)(react@19.1.0) + react: 19.1.0 + react-reconciler: 0.31.0(react@19.1.0) + react-use-measure: 2.1.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + scheduler: 0.25.0 + suspend-react: 0.1.3(react@19.1.0) + three: 0.178.0 + use-sync-external-store: 1.5.0(react@19.1.0) + zustand: 5.0.6(@types/react@19.1.8)(immer@10.1.1)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + optionalDependencies: + react-dom: 19.1.0(react@19.1.0) + transitivePeerDependencies: + - '@types/react' + - immer + '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1))(react@19.1.0)': dependencies: '@standard-schema/spec': 1.0.0 @@ -7419,6 +7755,8 @@ snapshots: dependencies: typescript: 5.8.3 + '@tweenjs/tween.js@23.1.3': {} + '@types/babel__code-frame@7.0.6': {} '@types/babel__core@7.20.5': @@ -7470,6 +7808,8 @@ snapshots: '@types/d3-timer@3.0.2': {} + '@types/draco3d@1.4.10': {} + '@types/estree@1.0.8': {} '@types/node@24.0.10': @@ -7478,26 +7818,46 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/offscreencanvas@2019.7.3': {} + '@types/react-dom@19.1.6(@types/react@19.1.8)': dependencies: '@types/react': 19.1.8 + '@types/react-reconciler@0.28.9(@types/react@19.1.8)': + dependencies: + '@types/react': 19.1.8 + '@types/react@19.1.8': dependencies: csstype: 3.1.3 '@types/resolve@1.20.2': {} + '@types/stats.js@0.17.4': {} + '@types/tern@0.23.9': dependencies: '@types/estree': 1.0.8 + '@types/three@0.178.0': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.22 + '@webgpu/types': 0.1.64 + fflate: 0.8.2 + meshoptimizer: 0.18.1 + '@types/triple-beam@1.3.5': {} '@types/trusted-types@2.0.7': {} '@types/use-sync-external-store@0.0.6': {} + '@types/webxr@0.5.22': {} + '@types/yauzl@2.10.3': dependencies: '@types/node': 24.0.10 @@ -7539,6 +7899,13 @@ snapshots: '@typescript-eslint/types': 8.36.0 eslint-visitor-keys: 4.2.1 + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@19.1.0)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 19.1.0 + '@vercel/nft@0.29.4(rollup@4.44.2)': dependencies: '@mapbox/node-pre-gyp': 2.0.0 @@ -7604,6 +7971,8 @@ snapshots: '@vue/shared@3.5.17': {} + '@webgpu/types@0.1.64': {} + '@whatwg-node/disposablestack@0.0.6': dependencies: '@whatwg-node/promise-helpers': 1.3.2 @@ -7719,6 +8088,10 @@ snapshots: base64-js@1.5.1: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} bindings@1.5.0: @@ -7784,6 +8157,10 @@ snapshots: callsite@1.0.0: {} + camera-controls@2.10.1(three@0.178.0): + dependencies: + three: 0.178.0 + caniuse-lite@1.0.30001727: {} chalk@4.1.2: @@ -7963,6 +8340,10 @@ snapshots: croner@9.1.0: {} + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -8049,6 +8430,10 @@ snapshots: destr@2.0.5: {} + detect-gpu@5.0.70: + dependencies: + webgl-constants: 1.1.1 + detect-libc@1.0.3: {} detect-libc@2.0.4: {} @@ -8141,6 +8526,8 @@ snapshots: dotenv@16.6.1: {} + draco3d@1.5.7: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -8345,6 +8732,10 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + fflate@0.6.10: {} + + fflate@0.8.2: {} + file-uri-to-path@1.0.0: {} fill-range@7.1.1: @@ -8455,6 +8846,8 @@ snapshots: globrex@0.1.2: {} + glsl-noise@0.0.0: {} + gonzales-pe@4.3.0: dependencies: minimist: 1.2.8 @@ -8504,6 +8897,8 @@ snapshots: dependencies: function-bind: 1.1.2 + hls.js@1.6.7: {} + hookable@5.5.3: {} hosted-git-info@7.0.2: @@ -8546,6 +8941,8 @@ snapshots: ignore@7.0.5: {} + immediate@3.0.6: {} + immer@10.1.1: {} imurmurhash@0.1.4: {} @@ -8610,6 +9007,8 @@ snapshots: is-plain-obj@2.1.0: {} + is-promise@2.2.2: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.8 @@ -8642,6 +9041,13 @@ snapshots: isexe@2.0.0: {} + its-fine@2.0.0(@types/react@19.1.8)(react@19.1.0): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@19.1.8) + react: 19.1.0 + transitivePeerDependencies: + - '@types/react' + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -8687,6 +9093,10 @@ snapshots: dependencies: readable-stream: 2.3.8 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-darwin-arm64@1.30.1: optional: true @@ -8826,6 +9236,11 @@ snapshots: luxon@3.6.1: {} + maath@0.10.8(@types/three@0.178.0)(three@0.178.0): + dependencies: + '@types/three': 0.178.0 + three: 0.178.0 + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.4 @@ -8848,6 +9263,12 @@ snapshots: merge2@1.4.1: {} + meshline@3.3.1(three@0.178.0): + dependencies: + three: 0.178.0 + + meshoptimizer@0.18.1: {} + micro-api-client@3.3.0: {} micromatch@4.0.8: @@ -9240,6 +9661,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + potpack@1.0.2: {} + precinct@12.2.0: dependencies: '@dependents/detective-less': 5.0.1 @@ -9268,6 +9691,11 @@ snapshots: process@0.11.10: {} + promise-worker-transferable@1.0.4: + dependencies: + is-promise: 2.2.2 + lie: 3.3.0 + pump@3.0.3: dependencies: end-of-stream: 1.4.5 @@ -9308,6 +9736,11 @@ snapshots: react-is@19.1.0: {} + react-reconciler@0.31.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.25.0 + react-redux@9.2.0(@types/react@19.1.8)(react@19.1.0)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 @@ -9346,6 +9779,12 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 + react-use-measure@2.1.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + optionalDependencies: + react-dom: 19.1.0(react@19.1.0) + react@19.1.0: {} read-package-up@11.0.0: @@ -9442,6 +9881,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + require-package-name@2.0.1: {} reselect@5.1.1: {} @@ -9515,6 +9956,8 @@ snapshots: safer-buffer@2.1.2: {} + scheduler@0.25.0: {} + scheduler@0.26.0: {} scule@1.3.0: {} @@ -9654,6 +10097,13 @@ snapshots: state-local@1.0.7: {} + stats-gl@2.4.2(@types/three@0.178.0)(three@0.178.0): + dependencies: + '@types/three': 0.178.0 + three: 0.178.0 + + stats.js@0.17.0: {} + statuses@2.0.1: {} statuses@2.0.2: {} @@ -9713,6 +10163,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + suspend-react@0.1.3(react@19.1.0): + dependencies: + react: 19.1.0 + system-architecture@0.1.0: {} tailwind-merge@3.3.1: {} @@ -9753,6 +10207,22 @@ snapshots: text-hex@1.0.0: {} + three-mesh-bvh@0.8.3(three@0.178.0): + dependencies: + three: 0.178.0 + + three-stdlib@2.36.0(three@0.178.0): + dependencies: + '@types/draco3d': 1.4.10 + '@types/offscreencanvas': 2019.7.3 + '@types/webxr': 0.5.22 + draco3d: 1.5.7 + fflate: 0.6.10 + potpack: 1.0.2 + three: 0.178.0 + + three@0.178.0: {} + tiny-invariant@1.3.3: {} tiny-warning@1.0.3: {} @@ -9784,6 +10254,20 @@ snapshots: triple-beam@1.4.1: {} + troika-three-text@0.52.4(three@0.178.0): + dependencies: + bidi-js: 1.0.3 + three: 0.178.0 + troika-three-utils: 0.52.4(three@0.178.0) + troika-worker-utils: 0.52.0 + webgl-sdf-generator: 1.1.1 + + troika-three-utils@0.52.4(three@0.178.0): + dependencies: + three: 0.178.0 + + troika-worker-utils@0.52.0: {} + ts-api-utils@2.1.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -9801,6 +10285,14 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel-rat@0.1.2(@types/react@19.1.8)(immer@10.1.1)(react@19.1.0): + dependencies: + zustand: 4.5.7(@types/react@19.1.8)(immer@10.1.1)(react@19.1.0) + transitivePeerDependencies: + - '@types/react' + - immer + - react + type-fest@4.41.0: {} typescript@5.8.3: {} @@ -9949,6 +10441,8 @@ snapshots: util-deprecate@1.0.2: {} + utility-types@3.11.0: {} + uuid@11.1.0: {} validate-npm-package-license@3.0.4: @@ -10031,6 +10525,10 @@ snapshots: web-streams-polyfill@3.3.3: {} + webgl-constants@1.1.1: {} + + webgl-sdf-generator@1.1.1: {} + webidl-conversions@3.0.1: {} webpack-virtual-modules@0.6.2: {} @@ -10141,3 +10639,18 @@ snapshots: readable-stream: 4.7.0 zod@3.25.75: {} + + zustand@4.5.7(@types/react@19.1.8)(immer@10.1.1)(react@19.1.0): + dependencies: + use-sync-external-store: 1.5.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.8 + immer: 10.1.1 + react: 19.1.0 + + zustand@5.0.6(@types/react@19.1.8)(immer@10.1.1)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)): + optionalDependencies: + '@types/react': 19.1.8 + immer: 10.1.1 + react: 19.1.0 + use-sync-external-store: 1.5.0(react@19.1.0) From 6957ee2062b17997f1cd498bbf68eee65f5c3e33 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 10 Jul 2025 01:32:11 +0000 Subject: [PATCH 2/2] Initialize Starfleet monorepo with core infrastructure and CLI tooling Co-authored-by: deslittle --- LICENSE | 21 + README.md | 385 ++++++++++------ cmd/gateway/main.go | 131 ++++++ go.mod | 43 ++ internal/providers/provider.go | 194 ++++++++ internal/server/server.go | 191 ++++++++ pkg/types/types.go | 58 +++ starfleet/LICENSE | 21 + starfleet/README.md | 91 ++++ starfleet/package.json | 35 ++ starfleet/packages/builder-three/package.json | 55 +++ .../builder-three/src/context/store.ts | 430 ++++++++++++++++++ .../packages/builder-three/src/index.tsx | 34 ++ starfleet/packages/cli/dev-app/index.html | 61 +++ starfleet/packages/cli/dev-app/package.json | 22 + starfleet/packages/cli/dev-app/src/DevApp.tsx | 100 ++++ starfleet/packages/cli/dev-app/src/main.tsx | 11 + starfleet/packages/cli/package.json | 54 +++ starfleet/packages/cli/src/cli.ts | 38 ++ starfleet/packages/cli/src/commands/dev.ts | 78 ++++ .../packages/cli/src/commands/generate.ts | 43 ++ starfleet/packages/cli/src/commands/init.ts | 171 +++++++ starfleet/packages/cli/src/importer/json.ts | 89 ++++ starfleet/packages/cli/src/importer/runner.ts | 57 +++ starfleet/packages/cli/src/importer/svg.ts | 87 ++++ .../packages/cli/src/importer/terraform.ts | 128 ++++++ starfleet/packages/cli/src/index.ts | 5 + starfleet/packages/cli/tsconfig.json | 24 + starfleet/packages/cli/tsup.config.ts | 16 + starfleet/packages/shared/package.json | 40 ++ starfleet/packages/shared/src/index.ts | 74 +++ starfleet/packages/shared/src/types.ts | 273 +++++++++++ starfleet/packages/shared/src/utils.ts | 155 +++++++ starfleet/packages/shared/tsconfig.json | 23 + starfleet/packages/shared/tsup.config.ts | 15 + starfleet/pnpm-workspace.yaml | 3 + 36 files changed, 3108 insertions(+), 148 deletions(-) create mode 100644 LICENSE create mode 100644 cmd/gateway/main.go create mode 100644 go.mod create mode 100644 internal/providers/provider.go create mode 100644 internal/server/server.go create mode 100644 pkg/types/types.go create mode 100644 starfleet/LICENSE create mode 100644 starfleet/README.md create mode 100644 starfleet/package.json create mode 100644 starfleet/packages/builder-three/package.json create mode 100644 starfleet/packages/builder-three/src/context/store.ts create mode 100644 starfleet/packages/builder-three/src/index.tsx create mode 100644 starfleet/packages/cli/dev-app/index.html create mode 100644 starfleet/packages/cli/dev-app/package.json create mode 100644 starfleet/packages/cli/dev-app/src/DevApp.tsx create mode 100644 starfleet/packages/cli/dev-app/src/main.tsx create mode 100644 starfleet/packages/cli/package.json create mode 100644 starfleet/packages/cli/src/cli.ts create mode 100644 starfleet/packages/cli/src/commands/dev.ts create mode 100644 starfleet/packages/cli/src/commands/generate.ts create mode 100644 starfleet/packages/cli/src/commands/init.ts create mode 100644 starfleet/packages/cli/src/importer/json.ts create mode 100644 starfleet/packages/cli/src/importer/runner.ts create mode 100644 starfleet/packages/cli/src/importer/svg.ts create mode 100644 starfleet/packages/cli/src/importer/terraform.ts create mode 100644 starfleet/packages/cli/src/index.ts create mode 100644 starfleet/packages/cli/tsconfig.json create mode 100644 starfleet/packages/cli/tsup.config.ts create mode 100644 starfleet/packages/shared/package.json create mode 100644 starfleet/packages/shared/src/index.ts create mode 100644 starfleet/packages/shared/src/types.ts create mode 100644 starfleet/packages/shared/src/utils.ts create mode 100644 starfleet/packages/shared/tsconfig.json create mode 100644 starfleet/packages/shared/tsup.config.ts create mode 100644 starfleet/pnpm-workspace.yaml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..844bae0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Hyperdrive Technology + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index dd992ec..b62b4e4 100644 --- a/README.md +++ b/README.md @@ -1,219 +1,308 @@ -# Hyperdrive +# Hyperdrive Technology -Hyperdrive is a modern, open-source PLC runtime with support for IEC 61131-3 programming languages and online changes. +Open-source infrastructure visualization and industrial control systems. -> 🚧 This repo is still under construction. 🚧 +## 🚀 Project Overview -## Features +Hyperdrive Technology develops modern, visual tools for infrastructure monitoring and industrial automation. Our flagship project **Starfleet** provides a React Three Fiber-based visual builder that transforms infrastructure definitions into interactive 3D scenes. -- Full support for IEC 61131-3 programming languages -- Real-time control and monitoring -- Online program changes -- WebSocket-based communication for UI updates -- Modern React-based UI with Monaco-powered editor -- Deploy to embedded devices, desktop or cloud -- Comprehensive documentation +## 📦 Repository Structure -## Project Structure +### Core Starfleet Monorepo (MIT License) -``` -hyperdrive/ -├── apps/ -│ ├── runtime/ # PLC runtime (Go) -│ └── ui/ # IDE + SCADA UI (TanStack Start + tRPC + Shadcn UI) -├── packages/ -│ ├── api/ # OpenAPI definitions -│ ├── api-pubsub/ # AsyncAPI definitions -│ ├── ide/ # Monaco-based IDE (TypeScript) -│ └── iec61131/ # Chevrotain-based parser for IEC 61131-3 -├── docs/ # Documentation -└── website/ # Marketing site -``` +**[starfleet](./starfleet)** - Main monorepo containing: +- `packages/cli` - Command-line interface for development and scene generation +- `packages/shared` - Shared TypeScript types and utilities +- `packages/builder-three` - React Three Fiber visual builder component +- `apps/studio` - Development studio application -## Development +### Gateway & Runtime Services (MIT License) -### Prerequisites +**[starfleet-gateway](./starfleet-gateway)** - Go service for real-time data aggregation +- WebSocket and REST APIs +- Multi-provider data aggregation +- Redis caching +- Health monitoring + +### Open Source Providers (MIT License) + +**[starfleet-provider-otel](./starfleet-provider-otel)** - OpenTelemetry data provider +- OTLP endpoint support +- Metrics, traces, and logs +- Real-time streaming + +**[starfleet-provider-hyperdrive](./starfleet-provider-hyperdrive)** - Hyperdrive PLC runtime provider +- Live PLC variable values +- WebSocket connection +- Variable subscription + +### Open Source Importers (MIT License) + +**[starfleet-importer-tf](./starfleet-importer-tf)** - Terraform to 3D scene converter +- Multi-cloud support (AWS, Azure, GCP) +- Custom layouts and positioning +- Live data integration + +### Commercial Components (Commercial License) + +**starfleet-pro** *(Private Repository)* +- Advanced 3D renderer with camera transitions +- Compressed 3D assets (GLB, Draco) +- Licence management SDK +- Enterprise features -- Node.js 23.6+ -- Go 1.24+ -- Docker & Docker Compose -- Gokrazy (for runtime deployment) -- Caddy (for SSL and reverse proxy) +**starfleet-importer-brainboard** *(Commercial)* +- Brainboard SVG to 3D scene conversion +- Advanced diagram parsing +- Enterprise integrations -### Getting Started +**starfleet-provider-datadog** *(Commercial)* +- DataDog metrics and logs integration +- Advanced analytics +- Enterprise monitoring -Clone the repository: +## 🎯 Quick Start + +### 1. Install Starfleet CLI ```bash -git clone https://github.com/hyperdrive-technology/hyperdrive.git -cd hyperdrive +npm install -g @starfleet/cli ``` -Install dependencies: +### 2. Initialize a New Project ```bash -npm install +starfleet init ``` -Start the development environment: +### 3. Import Infrastructure ```bash -docker-compose up +# From Terraform +starfleet dev --input infrastructure.tf + +# From existing scene +starfleet dev --input scene.json ``` -Access the applications: +### 4. Embed in React App -- UI: http://localhost:8080 -- Documentation: http://localhost:3002 -- Website: http://localhost:3001 -- IDE: http://localhost:3003 +```bash +npm install @hyperdrive/builder-three three @react-three/fiber @react-three/drei zustand @react-spring/three +``` -## Deployment +```tsx +import { HyperdriveProvider, HyperdriveEditor } from '@hyperdrive/builder-three'; + +export default function App() { + return ( + + + + ); +} +``` -### Runtime Deployment with Gokrazy +## 🏗️ Architecture -The Go-based runtime can be deployed to embedded devices using Gokrazy, a minimal Go-only operating system. +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ React Apps │ │ Starfleet │ │ Data Sources │ +│ (Builder-3) │◄───┤ Gateway │◄───┤ Providers │ +│ Web/Mobile │ │ (Go Service) │ │ OTEL/DD/HyperD│ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + ▲ ▲ ▲ + │ │ │ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Starfleet │ │ Importers │ │ Live Data │ +│ CLI Dev Srv │ │ TF/SVG/etc │ │ Streaming │ +│ (Vite) │ │ (Go/TS) │ │ WebSocket │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` -Install Gokrazy: +## 🚀 Features -```bash -go install github.com/gokrazy/tools/cmd/gokr-packer@latest -``` +### Visual Builder +- **React Three Fiber**: Performant 3D rendering +- **Drag & Drop**: Intuitive scene editing +- **Live Data**: Real-time visualization +- **Multiple Views**: 2D, 3D, and isometric modes +- **Plugin System**: Extensible architecture -Build and deploy the runtime: +### Infrastructure Import +- **Terraform**: AWS, Azure, GCP resources +- **SVG Diagrams**: Brainboard, Lucidchart, etc. +- **Custom Formats**: Extensible importer system +- **Live Monitoring**: Connect to existing tools -```bash -gokr-packer -overwrite=/dev/sdX ./apps/runtime -``` +### Real-time Data +- **Multi-Provider**: OTEL, DataDog, Prometheus, custom +- **WebSocket Streaming**: Efficient real-time updates +- **Transform Functions**: Data processing and mapping +- **Caching**: Redis-based performance optimization -Replace /dev/sdX with the target device (e.g., an SD card for a Raspberry Pi). +### Development Experience +- **One-Command Dev Server**: `starfleet dev` +- **Hot Reloading**: Instant updates during development +- **TypeScript**: Full type safety +- **Component Library**: Reusable React components -Access the runtime on the device's IP address. +## 🛠️ Development -### SSL with Caddy +### Prerequisites -Use Caddy as a reverse proxy to provide SSL for the runtime and UI. +- Node.js 18+ +- Go 1.22+ +- pnpm 8+ +- Docker (optional) -Install Caddy: +### Clone and Setup ```bash -sudo apt install -y caddy +git clone https://github.com/hyperdrive-technology/starfleet.git +cd starfleet +pnpm install +pnpm build ``` -Configure Caddy (Caddyfile): +### Development Commands + +```bash +# Start all services +pnpm dev -````caddyfile +# Build all packages +pnpm build -hyperdrive.example.com { - reverse_proxy localhost:8080 - tls { - email your-email@example.com - } -} +# Run tests +pnpm test -runtime.example.com { - reverse_proxy localhost:5000 - tls { - email your-email@example.com - } -} -Start Caddy: +# Start CLI dev server +pnpm --filter @starfleet/cli dev --input scene.json +``` -```bash -sudo systemctl start caddy -```` +## 📚 Documentation -Your runtime and UI will now be accessible over HTTPS. +- **[Starfleet CLI Guide](./starfleet/packages/cli/README.md)** - Command-line interface +- **[Builder-Three API](./starfleet/packages/builder-three/README.md)** - React component library +- **[Gateway API](./starfleet-gateway/README.md)** - Data aggregation service +- **[Provider Development](./starfleet-provider-otel/README.md)** - Creating data providers +- **[Importer Development](./starfleet-importer-tf/README.md)** - Creating importers -## Development Commands +## 🔌 Ecosystem + +### Community Providers +- OpenTelemetry (OTEL) +- Hyperdrive PLC Runtime +- Prometheus (planned) +- InfluxDB (planned) + +### Community Importers +- Terraform (AWS/Azure/GCP) +- Kubernetes YAML (planned) +- Docker Compose (planned) + +### Commercial Extensions +- DataDog integration +- Brainboard SVG import +- Advanced 3D rendering +- Enterprise support + +## 🎨 Examples + +### Infrastructure Visualization +Transform your Terraform into interactive 3D: ```bash -pnpm run dev # Start all applications in development mode -pnpm run build # Build all applications -pnpm run test # Run tests -pnpm run lint # Run linting -pnpm run clean # Clean build artifacts +terraform plan -out=tfplan +terraform show -json tfplan > infrastructure.json +starfleet dev --input infrastructure.json ``` -## Architecture +### Industrial Control Systems +Visualize PLC data in real-time: -### Runtime - -The PLC runtime is written in Go and provides: +```bash +starfleet dev --input factory-layout.json --provider hyperdrive +``` -- IEC 61131-3 program execution -- Physical I/O handling -- WebSocket server for UI communication -- Modbus TCP server -- OPC UA server +### Custom Applications +Embed in your React app: -### UI +```tsx + + + +``` -The web UI is built with: +## 🤝 Contributing -- React -- TanStack Start -- tRPC -- Shadcn UI -- WebSocket communication for real-time updates -- Typed API (defined in @hyperdrive/api) +We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. -### IDE +### Repository Guidelines +- **MIT Licensed** repos: Open for community contributions +- **Commercial** repos: Internal development only +- **Shared Types**: Use `@hyperdrive/shared` for consistency +- **Testing**: Include tests for all new features -The IDE is based on Monaco editor and provides: +### Development Process +1. Fork the repository +2. Create a feature branch +3. Add tests and documentation +4. Submit a pull request +5. Participate in code review -- A modern, browser based, extensible development environment -- Chevrontain parser for IEC 61131-3 programming with full LSP support -- Syntax highlighting, autocompletion, and validation -- AST-based program representation for online changes -- Integration with the runtime for compilation and debugging +## 📄 Licensing -## Communication +### Open Source (MIT) +- starfleet (monorepo) +- starfleet-gateway +- starfleet-provider-otel +- starfleet-provider-hyperdrive +- starfleet-importer-tf -### Server-to-UI Communication +### Commercial +- starfleet-pro +- starfleet-importer-brainboard +- starfleet-provider-datadog -Protocol: WebSocket -Schema: Defined using AsyncAPI -Purpose: Real-time updates for variable values, program state, and diagnostics +See individual repository LICENSE files for details. -### Shared Types +## 🌟 Roadmap -Tool: Quicktype -Purpose: Generate shared types for both TypeScript and Go -Location: packages/api +### Phase 1: Foundation (Weeks 1-4) +- ✅ Core types and scene schema +- ✅ CLI with dev server and importers +- ✅ Basic React Three Fiber builder +- ✅ Gateway service architecture -## API Documentation +### Phase 2: Visual Builder (Weeks 5-8) +- 🔄 Complete R3F editor with drag-and-drop +- 🔄 Plugin system and registry +- 🔄 Camera transitions and view modes +- 🔄 Live data integration -OpenAPI -RESTful APIs for configuration and management are defined using OpenAPI. -Documentation is auto-generated and available at /docs. +### Phase 3: Ecosystem (Weeks 9-12) +- 🔄 Provider implementations +- 🔄 Advanced importers +- 🔄 Documentation and examples +- 🔄 Community templates -### AsyncAPI +### Phase 4: Enterprise (Future) +- 🔮 Commercial feature development +- 🔮 Enterprise support and licensing +- 🔮 Advanced 3D rendering +- 🔮 Marketplace and ecosystem growth -Real-time communication (WebSocket) is defined using AsyncAPI. -Code generation for clients and servers is supported. +## 🏢 About Hyperdrive Technology -## Contributing +We're building the future of infrastructure visualization and industrial automation. Our tools make complex systems understandable through interactive 3D experiences. -Fork the repository -Create your feature branch (git checkout -b feature/amazing-feature) -Commit your changes (git commit -m 'Add some amazing feature') -Push to the branch (git push origin feature/amazing-feature) -Open a Pull Request -License -This project is licensed under the AGPLv3 License with CLA - see the LICENSE file for details. +**Contact**: [hello@hyperdrive.technology](mailto:hello@hyperdrive.technology) -## Acknowledgments +--- -- IEC 61131-3 Standard -- TanStack Start (https://tanstack.com/start/latest) -- Chevrotain (https://chevrotain.io/) -- AsyncAPI (https://www.asyncapi.com/) -- OpenAPI (https://www.openapis.org/) -- Gokrazy (https://gokrazy.org/) -- Caddy (https://caddyserver.com/) -- Quicktype (https://quicktype.io/) -- Shadcn UI (https://ui.shadcn.com/) -- Lucide (https://lucide.dev/) -- Tailwind CSS (https://tailwindcss.com/) -- InfluxDB / Telegraf (https://influxdata.com/) +*Made with ❤️ by the Hyperdrive Technology team* diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go new file mode 100644 index 0000000..64b7e04 --- /dev/null +++ b/cmd/gateway/main.go @@ -0,0 +1,131 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" + + "github.com/hyperdrive-technology/starfleet-gateway/internal/server" +) + +var ( + version = "0.1.0" + commit = "unknown" + date = "unknown" +) + +func main() { + if err := newRootCmd().Execute(); err != nil { + log.Fatal(err) + } +} + +func newRootCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "starfleet-gateway", + Short: "Starfleet Gateway - Real-time data aggregation service", + Long: `Starfleet Gateway aggregates real-time data from multiple providers +and serves it to Starfleet visual builders via REST and WebSocket APIs.`, + Version: fmt.Sprintf("%s (commit: %s, built: %s)", version, commit, date), + RunE: runServer, + } + + // Global flags + cmd.PersistentFlags().String("config", "", "config file path") + cmd.PersistentFlags().String("port", "8080", "server port") + cmd.PersistentFlags().String("host", "0.0.0.0", "server host") + cmd.PersistentFlags().Bool("debug", false, "enable debug mode") + cmd.PersistentFlags().String("redis-url", "redis://localhost:6379", "Redis URL for caching") + cmd.PersistentFlags().String("cors-origin", "*", "CORS allowed origin") + + // Bind flags to viper + viper.BindPFlag("port", cmd.PersistentFlags().Lookup("port")) + viper.BindPFlag("host", cmd.PersistentFlags().Lookup("host")) + viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug")) + viper.BindPFlag("redis-url", cmd.PersistentFlags().Lookup("redis-url")) + viper.BindPFlag("cors-origin", cmd.PersistentFlags().Lookup("cors-origin")) + + // Environment variables + viper.AutomaticEnv() + viper.SetEnvPrefix("STARFLEET") + + return cmd +} + +func runServer(cmd *cobra.Command, args []string) error { + // Configure logging + var logger *zap.Logger + var err error + + if viper.GetBool("debug") { + logger, err = zap.NewDevelopment() + } else { + logger, err = zap.NewProduction() + } + if err != nil { + return fmt.Errorf("failed to create logger: %w", err) + } + defer logger.Sync() + + // Set Gin mode + if !viper.GetBool("debug") { + gin.SetMode(gin.ReleaseMode) + } + + // Create server + srv, err := server.New(server.Config{ + Host: viper.GetString("host"), + Port: viper.GetString("port"), + RedisURL: viper.GetString("redis-url"), + CORSOrigin: viper.GetString("cors-origin"), + Logger: logger, + }) + if err != nil { + return fmt.Errorf("failed to create server: %w", err) + } + + // Start server + addr := fmt.Sprintf("%s:%s", viper.GetString("host"), viper.GetString("port")) + logger.Info("Starting Starfleet Gateway", zap.String("addr", addr)) + + httpServer := &http.Server{ + Addr: addr, + Handler: srv.Handler(), + } + + // Start server in a goroutine + go func() { + if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logger.Fatal("Failed to start server", zap.Error(err)) + } + }() + + // Wait for interrupt signal + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + logger.Info("Shutting down server...") + + // Graceful shutdown + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := httpServer.Shutdown(ctx); err != nil { + logger.Error("Server forced to shutdown", zap.Error(err)) + return err + } + + logger.Info("Server exited") + return nil +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..91688dd --- /dev/null +++ b/go.mod @@ -0,0 +1,43 @@ +module github.com/hyperdrive-technology/starfleet-gateway + +go 1.22 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/gorilla/websocket v1.5.1 + github.com/prometheus/client_golang v1.17.0 + github.com/redis/go-redis/v9 v9.3.0 + github.com/spf13/cobra v1.8.0 + github.com/spf13/viper v1.18.2 + go.uber.org/zap v1.26.0 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) \ No newline at end of file diff --git a/internal/providers/provider.go b/internal/providers/provider.go new file mode 100644 index 0000000..cbe8328 --- /dev/null +++ b/internal/providers/provider.go @@ -0,0 +1,194 @@ +package providers + +import ( + "context" + "time" + + "github.com/hyperdrive-technology/starfleet-gateway/pkg/types" +) + +// Provider interface defines the contract for all data providers +type Provider interface { + // ID returns the unique identifier for this provider + ID() string + + // Name returns the human-readable name of this provider + Name() string + + // Description returns a description of what this provider does + Description() string + + // Version returns the version of this provider + Version() string + + // Initialize the provider with configuration + Initialize(config map[string]interface{}) error + + // Query retrieves data from this provider + Query(request types.QueryRequest) ([]types.DataPoint, error) + + // Subscribe to real-time data streams (optional) + Subscribe(ctx context.Context, resourceIDs []string, callback func([]types.DataPoint)) error + + // Health check for this provider + HealthCheck() error + + // Close any resources used by this provider + Close() error +} + +// Config represents provider configuration +type Config struct { + Type string `json:"type"` + Parameters map[string]interface{} `json:"parameters"` +} + +// Registry holds all registered providers +type Registry struct { + providers map[string]Provider +} + +// NewRegistry creates a new provider registry +func NewRegistry() *Registry { + return &Registry{ + providers: make(map[string]Provider), + } +} + +// Register adds a provider to the registry +func (r *Registry) Register(provider Provider) error { + r.providers[provider.ID()] = provider + return nil +} + +// Get retrieves a provider by ID +func (r *Registry) Get(id string) (Provider, bool) { + provider, exists := r.providers[id] + return provider, exists +} + +// List returns all registered providers +func (r *Registry) List() []Provider { + providers := make([]Provider, 0, len(r.providers)) + for _, provider := range r.providers { + providers = append(providers, provider) + } + return providers +} + +// BaseProvider provides common functionality for all providers +type BaseProvider struct { + id string + name string + description string + version string + config map[string]interface{} +} + +// NewBaseProvider creates a new base provider +func NewBaseProvider(id, name, description, version string) *BaseProvider { + return &BaseProvider{ + id: id, + name: name, + description: description, + version: version, + config: make(map[string]interface{}), + } +} + +// ID returns the provider ID +func (p *BaseProvider) ID() string { + return p.id +} + +// Name returns the provider name +func (p *BaseProvider) Name() string { + return p.name +} + +// Description returns the provider description +func (p *BaseProvider) Description() string { + return p.description +} + +// Version returns the provider version +func (p *BaseProvider) Version() string { + return p.version +} + +// Initialize sets up the provider with configuration +func (p *BaseProvider) Initialize(config map[string]interface{}) error { + p.config = config + return nil +} + +// GetConfig returns the provider configuration +func (p *BaseProvider) GetConfig() map[string]interface{} { + return p.config +} + +// GetConfigValue retrieves a configuration value +func (p *BaseProvider) GetConfigValue(key string) (interface{}, bool) { + value, exists := p.config[key] + return value, exists +} + +// GetConfigString retrieves a string configuration value +func (p *BaseProvider) GetConfigString(key string) (string, bool) { + value, exists := p.config[key] + if !exists { + return "", false + } + + str, ok := value.(string) + return str, ok +} + +// GetConfigInt retrieves an integer configuration value +func (p *BaseProvider) GetConfigInt(key string) (int, bool) { + value, exists := p.config[key] + if !exists { + return 0, false + } + + switch v := value.(type) { + case int: + return v, true + case float64: + return int(v), true + default: + return 0, false + } +} + +// GetConfigBool retrieves a boolean configuration value +func (p *BaseProvider) GetConfigBool(key string) (bool, bool) { + value, exists := p.config[key] + if !exists { + return false, false + } + + b, ok := value.(bool) + return b, ok +} + +// GetConfigDuration retrieves a duration configuration value +func (p *BaseProvider) GetConfigDuration(key string) (time.Duration, bool) { + value, exists := p.config[key] + if !exists { + return 0, false + } + + switch v := value.(type) { + case string: + if duration, err := time.ParseDuration(v); err == nil { + return duration, true + } + case int: + return time.Duration(v) * time.Second, true + case float64: + return time.Duration(v) * time.Second, true + } + + return 0, false +} \ No newline at end of file diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..ee15b42 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,191 @@ +package server + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "go.uber.org/zap" + + "github.com/hyperdrive-technology/starfleet-gateway/internal/providers" + "github.com/hyperdrive-technology/starfleet-gateway/pkg/types" +) + +type Config struct { + Host string + Port string + RedisURL string + CORSOrigin string + Logger *zap.Logger +} + +type Server struct { + config Config + router *gin.Engine + providers map[string]providers.Provider + upgrader websocket.Upgrader + logger *zap.Logger +} + +func New(config Config) (*Server, error) { + s := &Server{ + config: config, + providers: make(map[string]providers.Provider), + upgrader: websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true // Allow all origins in development + }, + }, + logger: config.Logger, + } + + s.setupRouter() + return s, nil +} + +func (s *Server) setupRouter() { + s.router = gin.New() + s.router.Use(gin.Recovery()) + s.router.Use(gin.LoggerWithConfig(gin.LoggerConfig{ + Formatter: func(param gin.LogFormatterParams) string { + return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n", + param.ClientIP, + param.TimeStamp.Format(time.RFC1123), + param.Method, + param.Path, + param.Request.Proto, + param.StatusCode, + param.Latency, + param.Request.UserAgent(), + param.ErrorMessage, + ) + }, + })) + + // CORS middleware + corsConfig := cors.DefaultConfig() + corsConfig.AllowOrigins = []string{s.config.CORSOrigin} + corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} + corsConfig.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization"} + s.router.Use(cors.New(corsConfig)) + + // Health check + s.router.GET("/health", s.healthCheck) + + // API routes + api := s.router.Group("/api/v1") + { + api.GET("/providers", s.listProviders) + api.POST("/query", s.queryData) + api.GET("/ws", s.handleWebSocket) + } +} + +func (s *Server) Handler() http.Handler { + return s.router +} + +func (s *Server) healthCheck(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "ok", + "timestamp": time.Now().UTC().Format(time.RFC3339), + "version": "0.1.0", + }) +} + +func (s *Server) listProviders(c *gin.Context) { + providerList := make([]string, 0, len(s.providers)) + for name := range s.providers { + providerList = append(providerList, name) + } + + c.JSON(http.StatusOK, gin.H{ + "providers": providerList, + }) +} + +func (s *Server) queryData(c *gin.Context) { + var req types.QueryRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid request format", + }) + return + } + + provider, exists := s.providers[req.ProviderID] + if !exists { + c.JSON(http.StatusNotFound, gin.H{ + "error": "Provider not found", + }) + return + } + + data, err := provider.Query(req) + if err != nil { + s.logger.Error("Provider query failed", + zap.String("provider", req.ProviderID), + zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Query failed", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "data": data, + }) +} + +func (s *Server) handleWebSocket(c *gin.Context) { + conn, err := s.upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + s.logger.Error("WebSocket upgrade failed", zap.Error(err)) + return + } + defer conn.Close() + + s.logger.Info("WebSocket connection established") + + for { + var msg types.WebSocketMessage + if err := conn.ReadJSON(&msg); err != nil { + s.logger.Error("WebSocket read error", zap.Error(err)) + break + } + + // Handle different message types + switch msg.Type { + case "subscribe": + // Handle subscription to data streams + s.handleSubscription(conn, msg) + case "unsubscribe": + // Handle unsubscription + s.handleUnsubscription(conn, msg) + case "ping": + // Handle ping + conn.WriteJSON(types.WebSocketMessage{ + Type: "pong", + Data: msg.Data, + }) + } + } +} + +func (s *Server) handleSubscription(conn *websocket.Conn, msg types.WebSocketMessage) { + // Implementation for handling subscriptions + s.logger.Info("Subscription request", zap.Any("data", msg.Data)) +} + +func (s *Server) handleUnsubscription(conn *websocket.Conn, msg types.WebSocketMessage) { + // Implementation for handling unsubscriptions + s.logger.Info("Unsubscription request", zap.Any("data", msg.Data)) +} + +func (s *Server) RegisterProvider(name string, provider providers.Provider) { + s.providers[name] = provider + s.logger.Info("Provider registered", zap.String("name", name)) +} \ No newline at end of file diff --git a/pkg/types/types.go b/pkg/types/types.go new file mode 100644 index 0000000..9548067 --- /dev/null +++ b/pkg/types/types.go @@ -0,0 +1,58 @@ +package types + +import ( + "time" +) + +// QueryRequest represents a request for data from a provider +type QueryRequest struct { + ProviderID string `json:"providerId" binding:"required"` + ResourceIDs []string `json:"resourceIds" binding:"required"` + From *time.Time `json:"from,omitempty"` + To *time.Time `json:"to,omitempty"` + Params map[string]string `json:"params,omitempty"` +} + +// DataPoint represents a single data point with timestamp +type DataPoint struct { + ResourceID string `json:"resourceId"` + Value interface{} `json:"value"` + Timestamp time.Time `json:"timestamp"` + Metadata interface{} `json:"metadata,omitempty"` +} + +// QueryResponse represents the response from a provider query +type QueryResponse struct { + Data []DataPoint `json:"data"` + Status string `json:"status"` + Error string `json:"error,omitempty"` +} + +// WebSocketMessage represents a WebSocket message +type WebSocketMessage struct { + Type string `json:"type"` + Data interface{} `json:"data"` +} + +// SubscriptionRequest represents a request to subscribe to data streams +type SubscriptionRequest struct { + ProviderID string `json:"providerId"` + ResourceIDs []string `json:"resourceIds"` +} + +// ProviderInfo represents information about a provider +type ProviderInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Version string `json:"version"` + Status string `json:"status"` +} + +// HealthStatus represents the health status of the gateway +type HealthStatus struct { + Status string `json:"status"` + Timestamp time.Time `json:"timestamp"` + Version string `json:"version"` + Providers map[string]interface{} `json:"providers"` +} \ No newline at end of file diff --git a/starfleet/LICENSE b/starfleet/LICENSE new file mode 100644 index 0000000..844bae0 --- /dev/null +++ b/starfleet/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Hyperdrive Technology + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/starfleet/README.md b/starfleet/README.md new file mode 100644 index 0000000..b8afaad --- /dev/null +++ b/starfleet/README.md @@ -0,0 +1,91 @@ +# Starfleet + +Visual builder for infrastructure and industrial control systems using React Three Fiber. + +## Overview + +Starfleet is a comprehensive toolkit for creating interactive 3D visualizations of infrastructure and industrial systems. It provides: + +- **Visual Builder**: React Three Fiber-based editor for creating 3D scenes +- **Live Data Integration**: Real-time data visualization from various sources +- **Importers**: Convert Terraform plans, SVG diagrams, and other formats to 3D scenes +- **Providers**: Connect to monitoring systems, databases, and industrial control systems +- **Plugin System**: Extensible architecture for custom components and behaviors + +## Architecture + +``` +starfleet/ +├─ packages/ +│ ├─ cli/ # Command-line interface +│ ├─ shared/ # Shared types and utilities +│ ├─ builder-three/ # React Three Fiber visual builder +│ ├─ importer-template/ # Template for creating importers +│ └─ provider-template/ # Template for creating providers +└─ apps/ + └─ studio/ # Development studio application +``` + +## Quick Start + +```bash +# Install dependencies +pnpm install + +# Start development +pnpm dev + +# Build all packages +pnpm build + +# Run the CLI +pnpm --filter @starfleet/cli dev --input scene.json +``` + +## Usage + +### Development Server + +```bash +npx @starfleet/cli dev --input ./infrastructure.tf --port 5173 +``` + +### Embedding in React App + +```tsx +import { HyperdriveProvider, HyperdriveEditor } from '@hyperdrive/builder-three'; + +export default function App() { + return ( + + + + ); +} +``` + +## Packages + +- **@starfleet/cli** - Command-line interface and development server +- **@hyperdrive/shared** - Shared TypeScript types and utilities +- **@hyperdrive/builder-three** - React Three Fiber visual builder component + +## Development + +This project uses: +- **pnpm** for package management +- **TypeScript** for type safety +- **tsup** for building packages +- **Vite** for development server +- **Changesets** for version management + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines. + +## License + +MIT License - see [LICENSE](LICENSE) for details. \ No newline at end of file diff --git a/starfleet/package.json b/starfleet/package.json new file mode 100644 index 0000000..5f18154 --- /dev/null +++ b/starfleet/package.json @@ -0,0 +1,35 @@ +{ + "name": "starfleet", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Visual builder for infrastructure and industrial control systems", + "repository": { + "type": "git", + "url": "https://github.com/hyperdrive-technology/starfleet.git" + }, + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "pnpm": ">=8.0.0" + }, + "scripts": { + "build": "pnpm -r build", + "dev": "pnpm -r --parallel dev", + "test": "pnpm -r test", + "lint": "pnpm -r lint", + "clean": "pnpm -r clean", + "changeset": "changeset", + "version-packages": "changeset version", + "release": "pnpm build && changeset publish" + }, + "devDependencies": { + "@changesets/cli": "^2.27.1", + "@types/node": "^20.10.5", + "typescript": "^5.3.3", + "tsup": "^8.0.1", + "vite": "^5.0.10", + "vitest": "^1.1.0" + }, + "packageManager": "pnpm@8.15.0" +} \ No newline at end of file diff --git a/starfleet/packages/builder-three/package.json b/starfleet/packages/builder-three/package.json new file mode 100644 index 0000000..33a18de --- /dev/null +++ b/starfleet/packages/builder-three/package.json @@ -0,0 +1,55 @@ +{ + "name": "@hyperdrive/builder-three", + "version": "0.1.0", + "type": "module", + "description": "React Three Fiber visual builder for Hyperdrive/Starfleet", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup src/index.tsx --format cjs,esm --dts", + "dev": "tsup src/index.tsx --format cjs,esm --dts --watch", + "clean": "rm -rf dist", + "test": "vitest", + "lint": "tsc --noEmit" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18", + "three": "^0.161", + "@react-three/fiber": "^8", + "@react-three/drei": "^9", + "zustand": "^4", + "@react-spring/three": "^9" + }, + "dependencies": { + "@hyperdrive/shared": "workspace:*" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@types/three": "^0.161.2", + "tsup": "^8.0.1", + "typescript": "^5.3.3", + "vitest": "^1.1.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/hyperdrive-technology/starfleet.git", + "directory": "packages/builder-three" + }, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/starfleet/packages/builder-three/src/context/store.ts b/starfleet/packages/builder-three/src/context/store.ts new file mode 100644 index 0000000..358b318 --- /dev/null +++ b/starfleet/packages/builder-three/src/context/store.ts @@ -0,0 +1,430 @@ +import { create } from 'zustand'; +import { subscribeWithSelector } from 'zustand/middleware'; +import { + SceneFile, + SceneNode, + SceneEdge, + HyperdriveAPI, + CameraConfig, + generateId, + SCENE_VERSION +} from '@hyperdrive/shared'; + +export interface HyperdriveState { + // Scene data + scene: SceneFile; + + // Editor state + mode: 'editor' | 'control'; + selectedNodes: string[]; + + // UI state + showGrid: boolean; + showStats: boolean; + isLoading: boolean; + + // Actions + actions: { + // Scene operations + loadScene: (scene: SceneFile) => void; + getScene: () => SceneFile; + exportScene: () => string; + + // Node operations + addNode: (node: Omit) => string; + updateNode: (id: string, updates: Partial) => void; + removeNode: (id: string) => void; + getNode: (id: string) => SceneNode | null; + getNodes: () => SceneNode[]; + + // Edge operations + addEdge: (edge: Omit) => string; + updateEdge: (id: string, updates: Partial) => void; + removeEdge: (id: string) => void; + getEdge: (id: string) => SceneEdge | null; + getEdges: () => SceneEdge[]; + + // Mode and camera + setMode: (mode: 'editor' | 'control') => void; + getMode: () => 'editor' | 'control'; + setCamera: (config: Partial) => void; + getCamera: () => CameraConfig; + + // Selection + selectNode: (id: string) => void; + selectMultiple: (ids: string[]) => void; + getSelection: () => string[]; + clearSelection: () => void; + + // Live data + applyLiveData: (data: Array<{ + resourceId: string; + value: any; + timestamp?: Date; + }>) => void; + + // UI actions + setShowGrid: (show: boolean) => void; + setShowStats: (show: boolean) => void; + setLoading: (loading: boolean) => void; + + // Events + eventListeners: Map void)[]>; + on: (event: string, callback: (...args: any[]) => void) => () => void; + emit: (event: string, ...args: any[]) => void; + }; +} + +const createInitialScene = (): SceneFile => ({ + version: SCENE_VERSION, + metadata: { + name: 'New Scene', + description: 'Created with Hyperdrive Builder', + author: 'Unknown', + created: new Date().toISOString(), + modified: new Date().toISOString(), + tags: [], + }, + scene: { + background: { r: 0.1, g: 0.1, b: 0.1 }, + lighting: { + ambient: { r: 0.4, g: 0.4, b: 0.4 }, + directional: { + color: { r: 1, g: 1, b: 1 }, + intensity: 1, + position: { x: 10, y: 10, z: 10 }, + }, + }, + grid: { + enabled: true, + size: 20, + divisions: 20, + color: { r: 0.2, g: 0.2, b: 0.2 }, + }, + environment: { + type: 'studio', + intensity: 0.4, + }, + }, + camera: { + type: 'isometric', + position: { x: 10, y: 10, z: 10 }, + target: { x: 0, y: 0, z: 0 }, + fov: 50, + near: 0.1, + far: 1000, + }, + nodes: [], + edges: [], + presets: [], + providers: [], + ui: { + toolbar: true, + inspector: true, + minimap: false, + }, + performance: { + enableStats: false, + enableAntialiasing: true, + shadowsEnabled: true, + }, +}); + +export const useHyperdriveStore = create()( + subscribeWithSelector((set, get) => ({ + // Initial state + scene: createInitialScene(), + mode: 'editor', + selectedNodes: [], + showGrid: true, + showStats: false, + isLoading: false, + + // Actions + actions: { + // Scene operations + loadScene: (scene: SceneFile) => { + set({ scene, isLoading: false }); + get().actions.emit('scene-loaded', scene); + }, + + getScene: () => get().scene, + + exportScene: () => JSON.stringify(get().scene, null, 2), + + // Node operations + addNode: (node: Omit) => { + const id = generateId(); + const newNode = { ...node, id }; + + set((state) => ({ + scene: { + ...state.scene, + nodes: [...state.scene.nodes, newNode], + metadata: { + ...state.scene.metadata, + modified: new Date().toISOString(), + }, + }, + })); + + get().actions.emit('node-added', newNode); + return id; + }, + + updateNode: (id: string, updates: Partial) => { + set((state) => ({ + scene: { + ...state.scene, + nodes: state.scene.nodes.map(node => + node.id === id ? { ...node, ...updates } : node + ), + metadata: { + ...state.scene.metadata, + modified: new Date().toISOString(), + }, + }, + })); + + get().actions.emit('node-updated', id, updates); + }, + + removeNode: (id: string) => { + set((state) => ({ + scene: { + ...state.scene, + nodes: state.scene.nodes.filter(node => node.id !== id), + edges: state.scene.edges.filter(edge => + edge.source !== id && edge.target !== id + ), + metadata: { + ...state.scene.metadata, + modified: new Date().toISOString(), + }, + }, + selectedNodes: state.selectedNodes.filter(nodeId => nodeId !== id), + })); + + get().actions.emit('node-removed', id); + }, + + getNode: (id: string) => { + return get().scene.nodes.find(node => node.id === id) || null; + }, + + getNodes: () => get().scene.nodes, + + // Edge operations + addEdge: (edge: Omit) => { + const id = generateId(); + const newEdge = { ...edge, id }; + + set((state) => ({ + scene: { + ...state.scene, + edges: [...state.scene.edges, newEdge], + metadata: { + ...state.scene.metadata, + modified: new Date().toISOString(), + }, + }, + })); + + get().actions.emit('edge-added', newEdge); + return id; + }, + + updateEdge: (id: string, updates: Partial) => { + set((state) => ({ + scene: { + ...state.scene, + edges: state.scene.edges.map(edge => + edge.id === id ? { ...edge, ...updates } : edge + ), + metadata: { + ...state.scene.metadata, + modified: new Date().toISOString(), + }, + }, + })); + + get().actions.emit('edge-updated', id, updates); + }, + + removeEdge: (id: string) => { + set((state) => ({ + scene: { + ...state.scene, + edges: state.scene.edges.filter(edge => edge.id !== id), + metadata: { + ...state.scene.metadata, + modified: new Date().toISOString(), + }, + }, + })); + + get().actions.emit('edge-removed', id); + }, + + getEdge: (id: string) => { + return get().scene.edges.find(edge => edge.id === id) || null; + }, + + getEdges: () => get().scene.edges, + + // Mode and camera + setMode: (mode: 'editor' | 'control') => { + set({ mode }); + get().actions.emit('mode-changed', mode); + }, + + getMode: () => get().mode, + + setCamera: (config: Partial) => { + set((state) => ({ + scene: { + ...state.scene, + camera: { ...state.scene.camera, ...config }, + }, + })); + + get().actions.emit('camera-changed', config); + }, + + getCamera: () => get().scene.camera, + + // Selection + selectNode: (id: string) => { + set({ selectedNodes: [id] }); + get().actions.emit('selection-changed', [id]); + }, + + selectMultiple: (ids: string[]) => { + set({ selectedNodes: ids }); + get().actions.emit('selection-changed', ids); + }, + + getSelection: () => get().selectedNodes, + + clearSelection: () => { + set({ selectedNodes: [] }); + get().actions.emit('selection-changed', []); + }, + + // Live data + applyLiveData: (data: Array<{ + resourceId: string; + value: any; + timestamp?: Date; + }>) => { + // Apply live data to nodes with matching bindings + const state = get(); + const updatedNodes = state.scene.nodes.map(node => { + if (!node.liveData) return node; + + const updatedNode = { ...node }; + + node.liveData.forEach(binding => { + const dataPoint = data.find(d => d.resourceId === binding.resourceId); + if (dataPoint) { + // Apply transformation and update node property + let transformedValue = dataPoint.value; + + if (binding.transform) { + switch (binding.transform.type) { + case 'linear': + if (binding.transform.inputRange && binding.transform.outputRange) { + const [inMin, inMax] = binding.transform.inputRange; + const [outMin, outMax] = binding.transform.outputRange; + transformedValue = outMin + (dataPoint.value - inMin) * (outMax - outMin) / (inMax - inMin); + } + break; + case 'step': + if (binding.transform.steps) { + const step = binding.transform.steps.find(s => s.value === dataPoint.value); + if (step) transformedValue = step.output; + } + break; + case 'custom': + if (binding.transform.customFunction) { + try { + const fn = new Function('value', binding.transform.customFunction); + transformedValue = fn(dataPoint.value); + } catch (e) { + console.warn('Custom transform function error:', e); + } + } + break; + } + } + + // Update the node property + const propertyPath = binding.property.split('.'); + let target = updatedNode as any; + + for (let i = 0; i < propertyPath.length - 1; i++) { + if (!target[propertyPath[i]]) target[propertyPath[i]] = {}; + target = target[propertyPath[i]]; + } + + target[propertyPath[propertyPath.length - 1]] = transformedValue; + } + }); + + return updatedNode; + }); + + set((state) => ({ + scene: { + ...state.scene, + nodes: updatedNodes, + }, + })); + + get().actions.emit('live-data-applied', data); + }, + + // UI actions + setShowGrid: (show: boolean) => { + set({ showGrid: show }); + }, + + setShowStats: (show: boolean) => { + set({ showStats: show }); + }, + + setLoading: (loading: boolean) => { + set({ isLoading: loading }); + }, + + // Events + eventListeners: new Map(), + + on: (event: string, callback: (...args: any[]) => void) => { + const listeners = get().actions.eventListeners; + if (!listeners.has(event)) { + listeners.set(event, []); + } + listeners.get(event)!.push(callback); + + // Return unsubscribe function + return () => { + const currentListeners = listeners.get(event); + if (currentListeners) { + const index = currentListeners.indexOf(callback); + if (index > -1) { + currentListeners.splice(index, 1); + } + } + }; + }, + + emit: (event: string, ...args: any[]) => { + const listeners = get().actions.eventListeners.get(event); + if (listeners) { + listeners.forEach(callback => callback(...args)); + } + }, + }, + })) +); \ No newline at end of file diff --git a/starfleet/packages/builder-three/src/index.tsx b/starfleet/packages/builder-three/src/index.tsx new file mode 100644 index 0000000..aa16625 --- /dev/null +++ b/starfleet/packages/builder-three/src/index.tsx @@ -0,0 +1,34 @@ +// Export the main provider and editor components +export { HyperdriveProvider } from './context/Provider.js'; +export { HyperdriveEditor } from './components/HyperdriveEditor.js'; + +// Export hooks for accessing the hyperdrive state +export { useHyperdrive } from './hooks/useHyperdrive.js'; + +// Export composable scene components (advanced usage) +export { Scene } from './components/Scene.js'; +export { Node } from './components/Node.js'; +export { Edge } from './components/Edge.js'; + +// Export plugin system +export { registerPlugin, getPlugins } from './plugins/registry.js'; + +// Export utility functions +export { theme } from './utils/theme.js'; + +// Re-export types from shared package +export type { + SceneFile, + SceneNode, + SceneEdge, + HyperdriveAPI, + EditorPlugin, + Vector3, + Vector2, + Color, + CameraConfig, + ViewPreset, + LiveDataBinding, + NodeAnimation, + AnimationKeyframe, +} from '@hyperdrive/shared'; \ No newline at end of file diff --git a/starfleet/packages/cli/dev-app/index.html b/starfleet/packages/cli/dev-app/index.html new file mode 100644 index 0000000..d47538e --- /dev/null +++ b/starfleet/packages/cli/dev-app/index.html @@ -0,0 +1,61 @@ + + + + + + Starfleet Development Server + + + +
+
+
+

Loading Starfleet Editor...

+
+
+ + + \ No newline at end of file diff --git a/starfleet/packages/cli/dev-app/package.json b/starfleet/packages/cli/dev-app/package.json new file mode 100644 index 0000000..dffa886 --- /dev/null +++ b/starfleet/packages/cli/dev-app/package.json @@ -0,0 +1,22 @@ +{ + "name": "starfleet-dev-app", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } +} \ No newline at end of file diff --git a/starfleet/packages/cli/dev-app/src/DevApp.tsx b/starfleet/packages/cli/dev-app/src/DevApp.tsx new file mode 100644 index 0000000..acd6670 --- /dev/null +++ b/starfleet/packages/cli/dev-app/src/DevApp.tsx @@ -0,0 +1,100 @@ +import React, { useState, useEffect } from 'react'; + +// Declare the global scene URL that's injected by Vite +declare const __SCENE_URL__: string; + +export function DevApp() { + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Check if scene file exists + fetch(__SCENE_URL__) + .then(response => { + if (!response.ok) { + throw new Error('Scene file not found. Run with --input to specify an input file.'); + } + return response.json(); + }) + .then(scene => { + setLoading(false); + console.log('Scene loaded:', scene); + }) + .catch(err => { + setError(err.message); + setLoading(false); + }); + }, []); + + if (loading) { + return ( +
+
+

Loading scene...

+
+ ); + } + + if (error) { + return ( +
+

Error Loading Scene

+

{error}

+
+

To fix this:

+
    +
  • Initialize a project: starfleet init
  • +
  • Or provide an input file: starfleet dev --input file.tf
  • +
+
+
+ ); + } + + return ( +
+
+

Starfleet Development Server

+

The visual builder will appear here once @hyperdrive/builder-three is implemented.

+

Scene URL: {__SCENE_URL__}

+
+ +
+

🚀 What's Next?

+

This development server is ready to host the React Three Fiber editor. The next steps are:

+ +
    +
  1. Build the @hyperdrive/builder-three package with React Three Fiber components
  2. +
  3. Import and use HyperdriveProvider and HyperdriveEditor in this dev app
  4. +
  5. Connect to live data sources via providers
  6. +
  7. Add importers for Terraform, SVG, and other formats
  8. +
+ +
+

🛠️ Development Commands

+
+

starfleet init - Initialize a new project

+

starfleet dev --input scene.json - Start dev server

+

starfleet generate --input file.tf - Generate scene from input

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/starfleet/packages/cli/dev-app/src/main.tsx b/starfleet/packages/cli/dev-app/src/main.tsx new file mode 100644 index 0000000..0c34eeb --- /dev/null +++ b/starfleet/packages/cli/dev-app/src/main.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { DevApp } from './DevApp'; + +const container = document.getElementById('root'); +if (!container) { + throw new Error('Root element not found'); +} + +const root = createRoot(container); +root.render(); \ No newline at end of file diff --git a/starfleet/packages/cli/package.json b/starfleet/packages/cli/package.json new file mode 100644 index 0000000..ff018d3 --- /dev/null +++ b/starfleet/packages/cli/package.json @@ -0,0 +1,54 @@ +{ + "name": "@starfleet/cli", + "version": "0.1.0", + "type": "module", + "description": "Command-line interface for Starfleet visual builder", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "bin": { + "starfleet": "dist/cli.js" + }, + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist", + "dev-app" + ], + "scripts": { + "build": "tsup src/cli.ts src/index.ts --format esm --dts", + "dev": "tsup src/cli.ts src/index.ts --format esm --dts --watch", + "clean": "rm -rf dist", + "test": "vitest", + "lint": "tsc --noEmit" + }, + "dependencies": { + "@hyperdrive/shared": "workspace:*", + "commander": "^11.1.0", + "chokidar": "^3.5.3", + "execa": "^8.0.1", + "vite": "^5.0.10", + "zod": "^3.22.4", + "chalk": "^5.3.0", + "ora": "^8.0.1", + "inquirer": "^9.2.12" + }, + "devDependencies": { + "@types/inquirer": "^9.0.7", + "tsup": "^8.0.1", + "typescript": "^5.3.3", + "vitest": "^1.1.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/hyperdrive-technology/starfleet.git", + "directory": "packages/cli" + }, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/starfleet/packages/cli/src/cli.ts b/starfleet/packages/cli/src/cli.ts new file mode 100644 index 0000000..53ecac0 --- /dev/null +++ b/starfleet/packages/cli/src/cli.ts @@ -0,0 +1,38 @@ +#!/usr/bin/env node + +import { Command } from 'commander'; +import chalk from 'chalk'; +import { devCommand } from './commands/dev.js'; +import { generateCommand } from './commands/generate.js'; +import { initCommand } from './commands/init.js'; +import { VERSION } from '@hyperdrive/shared'; + +const program = new Command(); + +program + .name('starfleet') + .description('Visual builder for infrastructure and industrial control systems') + .version(VERSION); + +// Add commands +program.addCommand(initCommand); +program.addCommand(devCommand); +program.addCommand(generateCommand); + +// Global options +program.option('--verbose', 'Enable verbose logging'); +program.option('--quiet', 'Suppress output'); + +// Error handling +program.on('error', (err) => { + console.error(chalk.red('Error:'), err.message); + process.exit(1); +}); + +// Parse command line arguments +program.parse(); + +// Show help if no command provided +if (!process.argv.slice(2).length) { + program.outputHelp(); +} \ No newline at end of file diff --git a/starfleet/packages/cli/src/commands/dev.ts b/starfleet/packages/cli/src/commands/dev.ts new file mode 100644 index 0000000..086577b --- /dev/null +++ b/starfleet/packages/cli/src/commands/dev.ts @@ -0,0 +1,78 @@ +import { Command } from 'commander'; +import { createServer } from 'vite'; +import { watch } from 'chokidar'; +import { resolve, dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import chalk from 'chalk'; +import ora from 'ora'; +import { runImporter } from '../importer/runner.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export const devCommand = new Command('dev') + .description('Start development server with hot-reloading') + .option('-i, --input ', 'Input file (Terraform, SVG, etc.)') + .option('-p, --port ', 'Port to run server on', '5173') + .option('-o, --output ', 'Output scene file', '.starfleet/scene.json') + .option('--no-open', 'Don\'t open browser automatically') + .action(async (options) => { + const spinner = ora('Starting development server...').start(); + + try { + // Ensure output directory exists + const outputPath = resolve(options.output); + + // Run initial import if input file provided + if (options.input) { + spinner.text = 'Processing input file...'; + await runImporter(options.input, outputPath); + spinner.text = 'Starting development server...'; + } + + // Create Vite server + const server = await createServer({ + root: join(__dirname, '../../dev-app'), + server: { + port: parseInt(options.port), + open: options.open, + }, + define: { + __SCENE_URL__: JSON.stringify('/scene.json'), + }, + publicDir: resolve('.starfleet'), + }); + + await server.listen(); + + spinner.succeed(`Development server running at http://localhost:${options.port}`); + + // Watch for changes + if (options.input) { + console.log(chalk.blue('Watching for changes...')); + + const watcher = watch(options.input, { + persistent: true, + ignoreInitial: true, + }); + + watcher.on('change', async () => { + const updateSpinner = ora('Reprocessing input file...').start(); + try { + await runImporter(options.input, outputPath); + updateSpinner.succeed('Input file reprocessed'); + + // Trigger hot reload + server.ws.send({ + type: 'full-reload', + }); + } catch (error) { + updateSpinner.fail(`Error reprocessing: ${error.message}`); + } + }); + } + + } catch (error) { + spinner.fail(`Failed to start development server: ${error.message}`); + process.exit(1); + } + }); \ No newline at end of file diff --git a/starfleet/packages/cli/src/commands/generate.ts b/starfleet/packages/cli/src/commands/generate.ts new file mode 100644 index 0000000..bd04f12 --- /dev/null +++ b/starfleet/packages/cli/src/commands/generate.ts @@ -0,0 +1,43 @@ +import { Command } from 'commander'; +import { readFile, writeFile, mkdir } from 'fs/promises'; +import { resolve, dirname } from 'path'; +import chalk from 'chalk'; +import ora from 'ora'; +import { runImporter } from '../importer/runner.js'; + +export const generateCommand = new Command('generate') + .description('Generate scene file from input') + .option('-i, --input ', 'Input file (required)') + .option('-o, --output ', 'Output scene file', '.starfleet/scene.json') + .option('-f, --format ', 'Output format (json|yaml)', 'json') + .option('--force', 'Overwrite existing output file') + .action(async (options) => { + const spinner = ora('Generating scene file...').start(); + + try { + if (!options.input) { + throw new Error('Input file is required. Use -i or --input to specify the file.'); + } + + const inputFile = resolve(options.input); + const outputFile = resolve(options.output); + + // Ensure output directory exists + await mkdir(dirname(outputFile), { recursive: true }); + + // Run importer + await runImporter(inputFile, outputFile); + + spinner.succeed(`Scene file generated: ${outputFile}`); + + console.log(chalk.green('\n✅ Generated:')); + console.log(chalk.gray(` ${outputFile}`)); + + console.log(chalk.blue('\n🚀 Next steps:')); + console.log(chalk.gray(' starfleet dev --input scene.json')); + + } catch (error) { + spinner.fail(`Failed to generate scene file: ${error.message}`); + process.exit(1); + } + }); \ No newline at end of file diff --git a/starfleet/packages/cli/src/commands/init.ts b/starfleet/packages/cli/src/commands/init.ts new file mode 100644 index 0000000..c665a4b --- /dev/null +++ b/starfleet/packages/cli/src/commands/init.ts @@ -0,0 +1,171 @@ +import { Command } from 'commander'; +import { mkdir, writeFile, access } from 'fs/promises'; +import { resolve } from 'path'; +import chalk from 'chalk'; +import ora from 'ora'; +import inquirer from 'inquirer'; +import { SCENE_VERSION } from '@hyperdrive/shared'; + +const defaultSceneFile = { + version: SCENE_VERSION, + metadata: { + name: 'New Scene', + description: 'Created with Starfleet CLI', + author: 'Unknown', + created: new Date().toISOString(), + modified: new Date().toISOString(), + tags: [], + }, + scene: { + background: { r: 0.1, g: 0.1, b: 0.1 }, + lighting: { + ambient: { r: 0.2, g: 0.2, b: 0.2 }, + directional: { + color: { r: 1, g: 1, b: 1 }, + intensity: 1, + position: { x: 10, y: 10, z: 10 }, + }, + }, + grid: { + enabled: true, + size: 20, + divisions: 20, + color: { r: 0.2, g: 0.2, b: 0.2 }, + }, + environment: { + type: 'studio', + intensity: 0.3, + }, + }, + camera: { + type: 'isometric', + position: { x: 10, y: 10, z: 10 }, + target: { x: 0, y: 0, z: 0 }, + fov: 50, + near: 0.1, + far: 1000, + }, + nodes: [], + edges: [], + presets: [ + { + name: 'Default', + position: { x: 10, y: 10, z: 10 }, + target: { x: 0, y: 0, z: 0 }, + fov: 50, + }, + ], + providers: [], + ui: { + toolbar: true, + inspector: true, + minimap: false, + }, + performance: { + enableStats: false, + enableAntialiasing: true, + shadowsEnabled: true, + }, +}; + +export const initCommand = new Command('init') + .description('Initialize a new Starfleet project') + .option('-n, --name ', 'Project name') + .option('-d, --description ', 'Project description') + .option('-a, --author ', 'Author name') + .option('-f, --force', 'Overwrite existing files') + .option('-y, --yes', 'Skip prompts and use defaults') + .action(async (options) => { + const spinner = ora('Initializing Starfleet project...').start(); + + try { + // Check if .starfleet directory already exists + const starfleetDir = resolve('.starfleet'); + const sceneFile = resolve(starfleetDir, 'scene.json'); + + try { + await access(starfleetDir); + if (!options.force) { + spinner.stop(); + const { confirm } = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: 'Starfleet project already exists. Overwrite?', + default: false, + }, + ]); + + if (!confirm) { + console.log(chalk.yellow('Initialization cancelled.')); + return; + } + } + } catch { + // Directory doesn't exist, continue + } + + // Gather project information + let projectInfo = { + name: options.name || 'New Scene', + description: options.description || 'Created with Starfleet CLI', + author: options.author || 'Unknown', + }; + + if (!options.yes && !options.name) { + spinner.stop(); + const answers = await inquirer.prompt([ + { + type: 'input', + name: 'name', + message: 'Project name:', + default: projectInfo.name, + }, + { + type: 'input', + name: 'description', + message: 'Project description:', + default: projectInfo.description, + }, + { + type: 'input', + name: 'author', + message: 'Author name:', + default: projectInfo.author, + }, + ]); + projectInfo = { ...projectInfo, ...answers }; + spinner.start('Creating project files...'); + } + + // Create directory + await mkdir(starfleetDir, { recursive: true }); + + // Create scene file + const sceneData = { + ...defaultSceneFile, + metadata: { + ...defaultSceneFile.metadata, + ...projectInfo, + created: new Date().toISOString(), + modified: new Date().toISOString(), + }, + }; + + await writeFile(sceneFile, JSON.stringify(sceneData, null, 2)); + + spinner.succeed('Starfleet project initialized successfully!'); + + console.log(chalk.green('\n✅ Created:')); + console.log(chalk.gray(` .starfleet/scene.json`)); + + console.log(chalk.blue('\n🚀 Next steps:')); + console.log(chalk.gray(' starfleet dev --input scene.json')); + console.log(chalk.gray(' # or import from existing files:')); + console.log(chalk.gray(' starfleet dev --input infrastructure.tf')); + + } catch (error) { + spinner.fail(`Failed to initialize project: ${error.message}`); + process.exit(1); + } + }); \ No newline at end of file diff --git a/starfleet/packages/cli/src/importer/json.ts b/starfleet/packages/cli/src/importer/json.ts new file mode 100644 index 0000000..cc803e3 --- /dev/null +++ b/starfleet/packages/cli/src/importer/json.ts @@ -0,0 +1,89 @@ +import { readFile } from 'fs/promises'; +import { SceneFile, SCENE_VERSION, isSceneNode } from '@hyperdrive/shared'; + +export async function importJson(inputFile: string): Promise { + const content = await readFile(inputFile, 'utf-8'); + const data = JSON.parse(content); + + // If it's already a valid scene file, return it + if (data.version && data.metadata && data.scene && data.camera) { + return data as SceneFile; + } + + // Otherwise, create a basic scene + const scene: SceneFile = { + version: SCENE_VERSION, + metadata: { + name: 'JSON Import', + description: `Imported from ${inputFile}`, + author: 'Starfleet CLI', + created: new Date().toISOString(), + modified: new Date().toISOString(), + tags: ['json', 'imported'], + }, + scene: { + background: { r: 0.1, g: 0.1, b: 0.1 }, + lighting: { + ambient: { r: 0.4, g: 0.4, b: 0.4 }, + directional: { + color: { r: 1, g: 1, b: 1 }, + intensity: 1, + position: { x: 10, y: 10, z: 10 }, + }, + }, + grid: { + enabled: true, + size: 20, + divisions: 20, + color: { r: 0.2, g: 0.2, b: 0.2 }, + }, + environment: { + type: 'studio', + intensity: 0.4, + }, + }, + camera: { + type: 'isometric', + position: { x: 10, y: 10, z: 10 }, + target: { x: 0, y: 0, z: 0 }, + fov: 50, + near: 0.1, + far: 1000, + }, + nodes: [], + edges: [], + presets: [ + { + name: 'Default', + position: { x: 10, y: 10, z: 10 }, + target: { x: 0, y: 0, z: 0 }, + fov: 50, + }, + ], + providers: [], + ui: { + toolbar: true, + inspector: true, + minimap: false, + }, + performance: { + enableStats: false, + enableAntialiasing: true, + shadowsEnabled: true, + }, + }; + + // If the JSON has nodes array, try to import them + if (Array.isArray(data.nodes)) { + scene.nodes = data.nodes.filter(isSceneNode); + } + + // If the JSON has edges array, try to import them + if (Array.isArray(data.edges)) { + scene.edges = data.edges.filter((edge: any) => + edge && typeof edge.id === 'string' && typeof edge.source === 'string' && typeof edge.target === 'string' + ); + } + + return scene; +} \ No newline at end of file diff --git a/starfleet/packages/cli/src/importer/runner.ts b/starfleet/packages/cli/src/importer/runner.ts new file mode 100644 index 0000000..baa4dd0 --- /dev/null +++ b/starfleet/packages/cli/src/importer/runner.ts @@ -0,0 +1,57 @@ +import { readFile, writeFile } from 'fs/promises'; +import { extname } from 'path'; +import { SceneFile } from '@hyperdrive/shared'; +import { importTerraform } from './terraform.js'; +import { importSvg } from './svg.js'; +import { importJson } from './json.js'; + +export async function runImporter(inputFile: string, outputFile: string): Promise { + const extension = extname(inputFile).toLowerCase(); + + let sceneData: SceneFile; + + switch (extension) { + case '.tf': + case '.tfplan': + case '.tfstate': + sceneData = await importTerraform(inputFile); + break; + + case '.svg': + sceneData = await importSvg(inputFile); + break; + + case '.json': + sceneData = await importJson(inputFile); + break; + + default: + throw new Error(`Unsupported file type: ${extension}`); + } + + // Update metadata + sceneData.metadata.modified = new Date().toISOString(); + + // Write output + await writeFile(outputFile, JSON.stringify(sceneData, null, 2)); +} + +export function getImporterForFile(filename: string): string | null { + const extension = extname(filename).toLowerCase(); + + switch (extension) { + case '.tf': + case '.tfplan': + case '.tfstate': + return 'terraform'; + + case '.svg': + return 'svg'; + + case '.json': + return 'json'; + + default: + return null; + } +} \ No newline at end of file diff --git a/starfleet/packages/cli/src/importer/svg.ts b/starfleet/packages/cli/src/importer/svg.ts new file mode 100644 index 0000000..4a73e4a --- /dev/null +++ b/starfleet/packages/cli/src/importer/svg.ts @@ -0,0 +1,87 @@ +import { readFile } from 'fs/promises'; +import { SceneFile, SCENE_VERSION, createNode, generateId } from '@hyperdrive/shared'; + +export async function importSvg(inputFile: string): Promise { + const content = await readFile(inputFile, 'utf-8'); + + const scene: SceneFile = { + version: SCENE_VERSION, + metadata: { + name: 'SVG Diagram', + description: `Imported from ${inputFile}`, + author: 'Starfleet CLI', + created: new Date().toISOString(), + modified: new Date().toISOString(), + tags: ['svg', 'diagram'], + }, + scene: { + background: { r: 0.95, g: 0.95, b: 0.95 }, + lighting: { + ambient: { r: 0.6, g: 0.6, b: 0.6 }, + directional: { + color: { r: 1, g: 1, b: 1 }, + intensity: 0.8, + position: { x: 5, y: 10, z: 5 }, + }, + }, + grid: { + enabled: false, + size: 20, + divisions: 20, + color: { r: 0.8, g: 0.8, b: 0.8 }, + }, + environment: { + type: 'studio', + intensity: 0.3, + }, + }, + camera: { + type: '2d', + position: { x: 0, y: 0, z: 10 }, + target: { x: 0, y: 0, z: 0 }, + fov: 50, + near: 0.1, + far: 100, + orthographic: true, + }, + nodes: [], + edges: [], + presets: [ + { + name: 'Top View', + position: { x: 0, y: 0, z: 10 }, + target: { x: 0, y: 0, z: 0 }, + fov: 50, + }, + ], + providers: [], + ui: { + toolbar: true, + inspector: true, + minimap: false, + }, + performance: { + enableStats: false, + enableAntialiasing: true, + shadowsEnabled: false, + }, + }; + + // Basic SVG parsing - this would be expanded with proper SVG parsing + scene.nodes.push({ + id: generateId(), + ...createNode('svg', { x: 0, y: 0, z: 0 }, { + name: 'SVG Content', + geometry: { + url: inputFile, + }, + style: { + color: { r: 1, g: 1, b: 1 }, + material: 'basic', + }, + tags: ['svg', 'imported'], + }), + }); + + return scene; +} \ No newline at end of file diff --git a/starfleet/packages/cli/src/importer/terraform.ts b/starfleet/packages/cli/src/importer/terraform.ts new file mode 100644 index 0000000..b47e9b3 --- /dev/null +++ b/starfleet/packages/cli/src/importer/terraform.ts @@ -0,0 +1,128 @@ +import { readFile } from 'fs/promises'; +import { SceneFile, SCENE_VERSION, createNode, generateId } from '@hyperdrive/shared'; + +export async function importTerraform(inputFile: string): Promise { + const content = await readFile(inputFile, 'utf-8'); + + // Basic scene structure + const scene: SceneFile = { + version: SCENE_VERSION, + metadata: { + name: 'Terraform Infrastructure', + description: `Imported from ${inputFile}`, + author: 'Starfleet CLI', + created: new Date().toISOString(), + modified: new Date().toISOString(), + tags: ['terraform', 'infrastructure'], + }, + scene: { + background: { r: 0.05, g: 0.05, b: 0.1 }, + lighting: { + ambient: { r: 0.3, g: 0.3, b: 0.3 }, + directional: { + color: { r: 1, g: 1, b: 1 }, + intensity: 1, + position: { x: 10, y: 10, z: 10 }, + }, + }, + grid: { + enabled: true, + size: 50, + divisions: 50, + color: { r: 0.2, g: 0.2, b: 0.2 }, + }, + environment: { + type: 'outdoor', + intensity: 0.5, + }, + }, + camera: { + type: 'isometric', + position: { x: 20, y: 20, z: 20 }, + target: { x: 0, y: 0, z: 0 }, + fov: 50, + near: 0.1, + far: 1000, + }, + nodes: [], + edges: [], + presets: [ + { + name: 'Overview', + position: { x: 20, y: 20, z: 20 }, + target: { x: 0, y: 0, z: 0 }, + fov: 50, + }, + ], + providers: [], + ui: { + toolbar: true, + inspector: true, + minimap: true, + }, + performance: { + enableStats: false, + enableAntialiasing: true, + shadowsEnabled: true, + }, + }; + + // Basic parsing - this would be expanded with actual Terraform parsing + if (content.includes('aws_instance')) { + scene.nodes.push({ + id: generateId(), + ...createNode('box', { x: 0, y: 0, z: 0 }, { + name: 'AWS Instance', + style: { + color: { r: 1, g: 0.5, b: 0 }, + material: 'standard', + }, + geometry: { + width: 2, + height: 2, + depth: 2, + }, + tags: ['aws', 'instance'], + }), + }); + } + + if (content.includes('aws_s3_bucket')) { + scene.nodes.push({ + id: generateId(), + ...createNode('cylinder', { x: 5, y: 0, z: 0 }, { + name: 'S3 Bucket', + style: { + color: { r: 0.2, g: 0.8, b: 0.2 }, + material: 'standard', + }, + geometry: { + radius: 1.5, + height: 3, + }, + tags: ['aws', 's3', 'storage'], + }), + }); + } + + if (content.includes('aws_rds_instance')) { + scene.nodes.push({ + id: generateId(), + ...createNode('box', { x: -5, y: 0, z: 0 }, { + name: 'RDS Database', + style: { + color: { r: 0.3, g: 0.3, b: 0.8 }, + material: 'standard', + }, + geometry: { + width: 3, + height: 1, + depth: 2, + }, + tags: ['aws', 'rds', 'database'], + }), + }); + } + + return scene; +} \ No newline at end of file diff --git a/starfleet/packages/cli/src/index.ts b/starfleet/packages/cli/src/index.ts new file mode 100644 index 0000000..82256a6 --- /dev/null +++ b/starfleet/packages/cli/src/index.ts @@ -0,0 +1,5 @@ +// Export CLI utilities for programmatic use +export { runImporter, getImporterForFile } from './importer/runner.js'; +export { devCommand } from './commands/dev.js'; +export { initCommand } from './commands/init.js'; +export { generateCommand } from './commands/generate.js'; \ No newline at end of file diff --git a/starfleet/packages/cli/tsconfig.json b/starfleet/packages/cli/tsconfig.json new file mode 100644 index 0000000..bdf6af4 --- /dev/null +++ b/starfleet/packages/cli/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Node", + "lib": ["ES2022", "DOM"], + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": false, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "dev-app", "**/*.test.ts", "**/*.spec.ts"] +} \ No newline at end of file diff --git a/starfleet/packages/cli/tsup.config.ts b/starfleet/packages/cli/tsup.config.ts new file mode 100644 index 0000000..36312db --- /dev/null +++ b/starfleet/packages/cli/tsup.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/cli.ts', 'src/index.ts'], + splitting: false, + sourcemap: true, + dts: true, + format: ['esm'], + target: 'es2022', + clean: true, + external: ['vite', 'chokidar', 'execa'], + banner: { + js: '/* Starfleet CLI - MIT License */', + }, + shims: true, +}); \ No newline at end of file diff --git a/starfleet/packages/shared/package.json b/starfleet/packages/shared/package.json new file mode 100644 index 0000000..3d480f7 --- /dev/null +++ b/starfleet/packages/shared/package.json @@ -0,0 +1,40 @@ +{ + "name": "@hyperdrive/shared", + "version": "0.1.0", + "type": "module", + "description": "Shared types and utilities for Hyperdrive/Starfleet", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup src/index.ts --format cjs,esm --dts", + "dev": "tsup src/index.ts --format cjs,esm --dts --watch", + "clean": "rm -rf dist", + "test": "vitest", + "lint": "tsc --noEmit" + }, + "devDependencies": { + "tsup": "^8.0.1", + "typescript": "^5.3.3", + "vitest": "^1.1.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/hyperdrive-technology/starfleet.git", + "directory": "packages/shared" + }, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/starfleet/packages/shared/src/index.ts b/starfleet/packages/shared/src/index.ts new file mode 100644 index 0000000..edb55ca --- /dev/null +++ b/starfleet/packages/shared/src/index.ts @@ -0,0 +1,74 @@ +// Export all types +export * from './types.js'; + +// Export all utilities +export * from './utils.js'; + +// Version information +export const VERSION = '0.1.0'; + +// Constants +export const SCENE_VERSION = '1.0.0'; + +// Common node types +export const NODE_TYPES = [ + 'box', + 'cylinder', + 'sphere', + 'cone', + 'plane', + 'group', + 'svg', + 'gltf' +] as const; + +// Common edge types +export const EDGE_TYPES = [ + 'line', + 'curve', + 'pipe', + 'signal' +] as const; + +// Common view modes +export const VIEW_MODES = [ + '2d', + '3d', + 'isometric' +] as const; + +// Common editor modes +export const EDITOR_MODES = [ + 'editor', + 'control' +] as const; + +// Material types +export const MATERIAL_TYPES = [ + 'basic', + 'standard', + 'physical' +] as const; + +// Environment types +export const ENVIRONMENT_TYPES = [ + 'studio', + 'outdoor', + 'indoor', + 'none' +] as const; + +// Animation easing types +export const EASING_TYPES = [ + 'linear', + 'ease-in', + 'ease-out', + 'ease-in-out' +] as const; + +// Transform types +export const TRANSFORM_TYPES = [ + 'linear', + 'step', + 'custom' +] as const; \ No newline at end of file diff --git a/starfleet/packages/shared/src/types.ts b/starfleet/packages/shared/src/types.ts new file mode 100644 index 0000000..d925690 --- /dev/null +++ b/starfleet/packages/shared/src/types.ts @@ -0,0 +1,273 @@ +// Core geometric types +export interface Vector3 { + x: number; + y: number; + z: number; +} + +export interface Vector2 { + x: number; + y: number; +} + +export interface Color { + r: number; + g: number; + b: number; + a?: number; +} + +export interface Transform { + position: Vector3; + rotation: Vector3; + scale: Vector3; +} + +// Animation types +export interface AnimationKeyframe { + time: number; + value: any; + easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'; +} + +export interface NodeAnimation { + property: string; + keyframes: AnimationKeyframe[]; + duration: number; + loop?: boolean; +} + +// Live data binding types +export interface LiveDataBinding { + resourceId: string; + property: string; + transform?: { + type: 'linear' | 'step' | 'custom'; + inputRange?: [number, number]; + outputRange?: [number, number]; + steps?: Array<{ value: number; output: any }>; + customFunction?: string; + }; +} + +// Scene node types +export interface NodeStyle { + color?: Color; + opacity?: number; + visible?: boolean; + wireframe?: boolean; + material?: 'basic' | 'standard' | 'physical'; +} + +export interface SceneNode { + id: string; + type: 'box' | 'cylinder' | 'sphere' | 'cone' | 'plane' | 'group' | 'svg' | 'gltf'; + position: Vector3; + rotation: Vector3; + scale: Vector3; + style: NodeStyle; + data?: Record; + children?: string[]; + parent?: string; + + // Type-specific properties + geometry?: { + width?: number; + height?: number; + depth?: number; + radius?: number; + segments?: number; + url?: string; // for SVG/GLTF + }; + + // Live data integration + liveData?: LiveDataBinding[]; + animations?: NodeAnimation[]; + + // Metadata + name?: string; + description?: string; + tags?: string[]; +} + +// Connection/Edge types +export interface SceneEdge { + id: string; + source: string; + target: string; + type: 'line' | 'curve' | 'pipe' | 'signal'; + style: { + color?: Color; + width?: number; + opacity?: number; + dashArray?: number[]; + }; + points?: Vector3[]; + liveData?: LiveDataBinding[]; +} + +// View and camera types +export interface ViewPreset { + name: string; + position: Vector3; + target: Vector3; + fov?: number; + near?: number; + far?: number; +} + +export interface CameraConfig { + type: '2d' | '3d' | 'isometric'; + position: Vector3; + target: Vector3; + fov: number; + near: number; + far: number; + orthographic?: boolean; + zoom?: number; +} + +// Scene configuration +export interface SceneConfig { + background?: Color; + fog?: { + enabled: boolean; + color: Color; + near: number; + far: number; + }; + lighting?: { + ambient?: Color; + directional?: { + color: Color; + intensity: number; + position: Vector3; + }; + }; + grid?: { + enabled: boolean; + size: number; + divisions: number; + color: Color; + }; + environment?: { + type: 'studio' | 'outdoor' | 'indoor' | 'none'; + intensity?: number; + }; +} + +// Complete scene file +export interface SceneFile { + version: string; + metadata: { + name: string; + description?: string; + author?: string; + created: string; + modified: string; + tags?: string[]; + }; + scene: SceneConfig; + camera: CameraConfig; + nodes: SceneNode[]; + edges: SceneEdge[]; + presets: ViewPreset[]; + providers?: string[]; + ui?: { + toolbar?: boolean; + inspector?: boolean; + minimap?: boolean; + }; + performance?: { + enableStats?: boolean; + enableAntialiasing?: boolean; + shadowsEnabled?: boolean; + }; +} + +// Plugin interfaces +export interface Importer { + id: string; + name: string; + description: string; + fileExtensions: string[]; + import(data: string | ArrayBuffer | Uint8Array): Promise; +} + +export interface Provider { + id: string; + name: string; + description: string; + init(config: Record): Promise; + query(params: { + resourceIds: string[]; + from?: Date; + to?: Date; + }): Promise>; + subscribe?(resourceIds: string[], callback: (data: any) => void): () => void; +} + +export interface AnimationHook { + id: string; + name: string; + description: string; + apply(node: SceneNode, value: any): any; +} + +// Editor plugin interface +export interface EditorPlugin { + id: string; + name: string; + description: string; + mount(container: HTMLElement, api: HyperdriveAPI): void; + unmount?(): void; +} + +// API interface for editor plugins +export interface HyperdriveAPI { + // Node operations + addNode(node: Omit): string; + updateNode(id: string, updates: Partial): void; + removeNode(id: string): void; + getNode(id: string): SceneNode | null; + getNodes(): SceneNode[]; + + // Edge operations + addEdge(edge: Omit): string; + updateEdge(id: string, updates: Partial): void; + removeEdge(id: string): void; + getEdge(id: string): SceneEdge | null; + getEdges(): SceneEdge[]; + + // Mode and camera + setMode(mode: 'editor' | 'control'): void; + getMode(): 'editor' | 'control'; + setCamera(config: Partial): void; + getCamera(): CameraConfig; + + // Live data + applyLiveData(data: Array<{ + resourceId: string; + value: any; + timestamp?: Date; + }>): void; + + // Scene operations + loadScene(scene: SceneFile): void; + getScene(): SceneFile; + exportScene(): string; + + // Selection + selectNode(id: string): void; + selectMultiple(ids: string[]): void; + getSelection(): string[]; + clearSelection(): void; + + // Events + on(event: string, callback: (...args: any[]) => void): () => void; + emit(event: string, ...args: any[]): void; +} \ No newline at end of file diff --git a/starfleet/packages/shared/src/utils.ts b/starfleet/packages/shared/src/utils.ts new file mode 100644 index 0000000..a261e33 --- /dev/null +++ b/starfleet/packages/shared/src/utils.ts @@ -0,0 +1,155 @@ +import { Vector3, Vector2, Color, SceneNode, SceneEdge } from './types.js'; + +// Vector utilities +export const vec3 = (x: number, y: number = x, z: number = x): Vector3 => ({ x, y, z }); +export const vec2 = (x: number, y: number = x): Vector2 => ({ x, y }); + +export const addVec3 = (a: Vector3, b: Vector3): Vector3 => ({ + x: a.x + b.x, + y: a.y + b.y, + z: a.z + b.z, +}); + +export const subVec3 = (a: Vector3, b: Vector3): Vector3 => ({ + x: a.x - b.x, + y: a.y - b.y, + z: a.z - b.z, +}); + +export const scaleVec3 = (v: Vector3, scale: number): Vector3 => ({ + x: v.x * scale, + y: v.y * scale, + z: v.z * scale, +}); + +export const lengthVec3 = (v: Vector3): number => + Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z); + +export const normalizeVec3 = (v: Vector3): Vector3 => { + const len = lengthVec3(v); + return len > 0 ? scaleVec3(v, 1 / len) : { x: 0, y: 0, z: 0 }; +}; + +// Color utilities +export const rgb = (r: number, g: number, b: number): Color => ({ r, g, b }); +export const rgba = (r: number, g: number, b: number, a: number): Color => ({ r, g, b, a }); + +export const hexToColor = (hex: string): Color => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16) / 255, + g: parseInt(result[2], 16) / 255, + b: parseInt(result[3], 16) / 255, + } : { r: 0, g: 0, b: 0 }; +}; + +export const colorToHex = (color: Color): string => { + const toHex = (c: number) => Math.round(c * 255).toString(16).padStart(2, '0'); + return `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`; +}; + +// Node utilities +export const createNode = ( + type: SceneNode['type'], + position: Vector3 = vec3(0), + options: Partial = {} +): Omit => ({ + type, + position, + rotation: vec3(0), + scale: vec3(1), + style: { + color: rgb(0.5, 0.5, 0.5), + opacity: 1, + visible: true, + material: 'standard', + }, + ...options, +}); + +export const createEdge = ( + source: string, + target: string, + type: SceneEdge['type'] = 'line', + options: Partial = {} +): Omit => ({ + source, + target, + type, + style: { + color: rgb(0.3, 0.3, 0.3), + width: 2, + opacity: 1, + }, + ...options, +}); + +// ID generation +export const generateId = (): string => { + return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); +}; + +// Validation utilities +export const isValidVector3 = (v: any): v is Vector3 => { + return v && typeof v.x === 'number' && typeof v.y === 'number' && typeof v.z === 'number'; +}; + +export const isValidColor = (c: any): c is Color => { + return c && typeof c.r === 'number' && typeof c.g === 'number' && typeof c.b === 'number'; +}; + +// Transform utilities +export const applyTransform = (point: Vector3, transform: { position: Vector3; rotation: Vector3; scale: Vector3 }): Vector3 => { + // Simple transform application (would need proper matrix math for rotation) + return { + x: (point.x * transform.scale.x) + transform.position.x, + y: (point.y * transform.scale.y) + transform.position.y, + z: (point.z * transform.scale.z) + transform.position.z, + }; +}; + +// Scene utilities +export const findNodeById = (nodes: SceneNode[], id: string): SceneNode | undefined => { + return nodes.find(node => node.id === id); +}; + +export const findEdgeById = (edges: SceneEdge[], id: string): SceneEdge | undefined => { + return edges.find(edge => edge.id === id); +}; + +export const getNodeChildren = (nodes: SceneNode[], parentId: string): SceneNode[] => { + return nodes.filter(node => node.parent === parentId); +}; + +export const getNodeConnections = (edges: SceneEdge[], nodeId: string): SceneEdge[] => { + return edges.filter(edge => edge.source === nodeId || edge.target === nodeId); +}; + +// Animation utilities +export const lerp = (a: number, b: number, t: number): number => a + (b - a) * t; + +export const easeInOut = (t: number): number => { + return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; +}; + +export const easeIn = (t: number): number => t * t; + +export const easeOut = (t: number): number => t * (2 - t); + +// Type guards +export const isSceneNode = (obj: any): obj is SceneNode => { + return obj && + typeof obj.id === 'string' && + typeof obj.type === 'string' && + isValidVector3(obj.position) && + isValidVector3(obj.rotation) && + isValidVector3(obj.scale); +}; + +export const isSceneEdge = (obj: any): obj is SceneEdge => { + return obj && + typeof obj.id === 'string' && + typeof obj.source === 'string' && + typeof obj.target === 'string' && + typeof obj.type === 'string'; +}; \ No newline at end of file diff --git a/starfleet/packages/shared/tsconfig.json b/starfleet/packages/shared/tsconfig.json new file mode 100644 index 0000000..6130612 --- /dev/null +++ b/starfleet/packages/shared/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Node", + "lib": ["ES2022", "DOM"], + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": false, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] +} \ No newline at end of file diff --git a/starfleet/packages/shared/tsup.config.ts b/starfleet/packages/shared/tsup.config.ts new file mode 100644 index 0000000..5db5788 --- /dev/null +++ b/starfleet/packages/shared/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + splitting: false, + sourcemap: true, + dts: true, + format: ['esm', 'cjs'], + target: 'es2022', + clean: true, + external: [], + banner: { + js: '/* Starfleet Shared - MIT License */', + }, +}); \ No newline at end of file diff --git a/starfleet/pnpm-workspace.yaml b/starfleet/pnpm-workspace.yaml new file mode 100644 index 0000000..4998798 --- /dev/null +++ b/starfleet/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "packages/*" + - "apps/*" \ No newline at end of file