From 510f71d4347c9b296a69ab6882dd66fab4434ebc Mon Sep 17 00:00:00 2001 From: Yonatan Hattav Date: Wed, 15 Jan 2025 20:16:00 +0200 Subject: [PATCH] Json editor (#86) * working panel. still buggy * more accurate validations * use zod * Use zod source of truth accross app * adjust default json --- package-lock.json | 103 +++++++++- package.json | 5 +- .../GravitySimulator/GravitySimulator.tsx | 14 ++ .../JsonScenarioPanel/JsonScenarioPanel.tsx | 186 ++++++++++++++++++ .../SimulatorControls/SimulatorControls.tsx | 12 +- src/schemas/physics.ts | 42 ++++ src/schemas/scenario.ts | 108 ++++++++++ src/types/particle.ts | 36 +--- src/types/scenario.ts | 19 +- src/utils/types/path.ts | 32 +-- src/utils/types/physics.ts | 37 ++-- 11 files changed, 493 insertions(+), 101 deletions(-) create mode 100644 src/components/JsonScenarioPanel/JsonScenarioPanel.tsx create mode 100644 src/schemas/physics.ts create mode 100644 src/schemas/scenario.ts diff --git a/package-lock.json b/package-lock.json index 4c37fc2..6e830cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@ant-design/icons": "^5.3.0", + "@monaco-editor/react": "^4.6.0", "@types/d3": "^7.4.3", "@types/html2canvas": "^0.5.35", "@types/lz-string": "^1.5.0", @@ -25,7 +26,9 @@ "react-dom": "^18.2.0", "react-icons": "^5.3.0", "react-router-dom": "^7.0.1", - "tone": "^15.0.4" + "tone": "^15.0.4", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0" }, "devDependencies": { "@chromatic-com/storybook": "^3.2.2", @@ -1902,6 +1905,32 @@ "react": ">=16" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -9727,6 +9756,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/monaco-editor": { + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "license": "MIT", + "peer": true + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -12221,6 +12257,12 @@ "tslib": "^2.7.0" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/std-env": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", @@ -14187,6 +14229,27 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.18.0" + } } }, "dependencies": { @@ -15437,6 +15500,22 @@ "@types/mdx": "^2.0.0" } }, + "@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "requires": { + "state-local": "^1.0.6" + } + }, + "@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "requires": { + "@monaco-editor/loader": "^1.4.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -20958,6 +21037,12 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, + "monaco-editor": { + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "peer": true + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -22614,6 +22699,11 @@ "tslib": "^2.7.0" } }, + "state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "std-env": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", @@ -23834,6 +23924,17 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==" + }, + "zod-validation-error": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", + "requires": {} } } } diff --git a/package.json b/package.json index db6b4a6..4cb6eaa 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@ant-design/icons": "^5.3.0", + "@monaco-editor/react": "^4.6.0", "@types/d3": "^7.4.3", "@types/html2canvas": "^0.5.35", "@types/lz-string": "^1.5.0", @@ -40,7 +41,9 @@ "react-dom": "^18.2.0", "react-icons": "^5.3.0", "react-router-dom": "^7.0.1", - "tone": "^15.0.4" + "tone": "^15.0.4", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0" }, "devDependencies": { "@chromatic-com/storybook": "^3.2.2", diff --git a/src/components/GravitySimulator/GravitySimulator.tsx b/src/components/GravitySimulator/GravitySimulator.tsx index e8670a5..fc1d7d1 100644 --- a/src/components/GravitySimulator/GravitySimulator.tsx +++ b/src/components/GravitySimulator/GravitySimulator.tsx @@ -50,6 +50,7 @@ import { useInteractionHandlers } from "../../hooks/useInteractionHandlers"; import { useSimulatorState } from "../../hooks/useSimulatorState"; import { useParticleSystem } from "../../hooks/useParticleSystem"; import { useScreenshot } from "../../hooks/useScreenshot"; +import { JsonScenarioPanel } from "../JsonScenarioPanel/JsonScenarioPanel"; export interface GravitySimulatorProps { gravityRef: React.RefObject; @@ -315,6 +316,12 @@ export const GravitySimulator: React.FC = ({ saveScenario, }); + const [isJsonPanelOpen, setIsJsonPanelOpen] = useState(false); + + const handleJsonPanelToggle = useCallback(() => { + setIsJsonPanelOpen((prev) => !prev); + }, []); + // Audio files definition const audioFiles = useMemo( () => [ @@ -546,6 +553,7 @@ export const GravitySimulator: React.FC = ({ onScreenshot={handleScreenshot} onScenarioPanel={handleScenarioPanelToggle} onSettingsPanel={handleSettingsPanelToggle} + onJsonPanel={handleJsonPanelToggle} isPaused={isPaused} isFullscreen={isFullscreen} isAudioPlaying={isAudioPlaying} @@ -599,6 +607,12 @@ export const GravitySimulator: React.FC = ({ onSelectScenario={onSelectScenario} /> + setIsJsonPanelOpen(false)} + onApplyScenario={handleSelectScenario} + /> + setIsSaveModalOpen(false)} diff --git a/src/components/JsonScenarioPanel/JsonScenarioPanel.tsx b/src/components/JsonScenarioPanel/JsonScenarioPanel.tsx new file mode 100644 index 0000000..2f2d1bc --- /dev/null +++ b/src/components/JsonScenarioPanel/JsonScenarioPanel.tsx @@ -0,0 +1,186 @@ +import React, { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import Editor, { OnChange } from "@monaco-editor/react"; +import { Scenario } from "../../types/scenario"; +import { IoClose } from "react-icons/io5"; +import { ScenarioSchema } from "../../schemas/scenario"; +import { fromZodError } from "zod-validation-error"; + +interface JsonScenarioPanelProps { + isOpen: boolean; + onClose: () => void; + onApplyScenario: (scenario: Scenario) => void; +} + +export const JsonScenarioPanel: React.FC = ({ + isOpen, + onClose, + onApplyScenario, +}) => { + const [jsonError, setJsonError] = useState(null); + const [editorContent, setEditorContent] = useState(`{ + "id": "custom-scenario", + "name": "Custom Scenario", + "description": "A custom scenario loaded from JSON", + "data": { + "settings": { + "NEW_PARTICLE_MASS": 0.02, + "NEW_PARTICLE_ELASTICITY": 0.8, + "FRICTION": 1, + "POINTER_MASS": 500000 + }, + "gravityPoints": [ + { + "x": 300, + "y": 300, + "label": "Custom Point", + "mass": 1000000 + } + ], + "particles": [ + { + "id": "particle-1", + "position": { "x": 200, "y": 200 }, + "velocity": { "x": 0, "y": 30 }, + "mass": 0.03, + "elasticity": 0.8 + } + ], + "paths": [] + } +}`); + + const handleEditorChange: OnChange = (value) => { + setEditorContent(value || ""); + }; + + const handleApply = () => { + try { + const parsedJson = JSON.parse(editorContent); + const result = ScenarioSchema.safeParse(parsedJson); + + if (!result.success) { + // Convert Zod error to a more readable format + const validationError = fromZodError(result.error); + setJsonError(validationError.message); + return; + } + + setJsonError(null); + onApplyScenario(result.data); + onClose(); + } catch (error) { + setJsonError( + error instanceof Error ? error.message : "Invalid JSON format" + ); + } + }; + + return ( + + {isOpen && ( + e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + className="floating-panel vertical-panel" + style={{ + width: "500px", + maxWidth: "90vw", + }} + > +
+
+

Load JSON Scenario

+ + + +
+
+ +
+
+ +
+ + {jsonError && ( +
+ {jsonError} +
+ )} + + + Apply Scenario + +
+
+ )} +
+ ); +}; diff --git a/src/components/SimulatorControls/SimulatorControls.tsx b/src/components/SimulatorControls/SimulatorControls.tsx index 66175bb..69e4878 100644 --- a/src/components/SimulatorControls/SimulatorControls.tsx +++ b/src/components/SimulatorControls/SimulatorControls.tsx @@ -7,6 +7,7 @@ import { AiOutlineExport } from "react-icons/ai"; import { VscLibrary } from "react-icons/vsc"; import { SettingOutlined } from "@ant-design/icons"; import { MusicPlayer } from "../MusicPlayer/MusicPlayer"; +import { IoCode } from "react-icons/io5"; interface SimulatorControlsProps { onPause: () => void; @@ -14,14 +15,15 @@ interface SimulatorControlsProps { onFullscreen: () => void; onExport: (e: React.MouseEvent) => void; onInvertColors: () => void; - onScreenshot: (e: React.MouseEvent) => void; + onScreenshot: (e: React.MouseEvent) => Promise; onScenarioPanel: (e: React.MouseEvent) => void; onSettingsPanel: (e: React.MouseEvent) => void; + onJsonPanel: (e: React.MouseEvent) => void; isPaused: boolean; isFullscreen: boolean; isAudioPlaying?: boolean; isAudioLoaded?: boolean; - onAudioToggle?: (e: React.MouseEvent) => void; + onAudioToggle?: (e: React.MouseEvent) => void; disableSound?: boolean; } @@ -62,6 +64,7 @@ export const SimulatorControls: React.FC = ({ onScreenshot, onScenarioPanel, onSettingsPanel, + onJsonPanel, isPaused, isFullscreen, isAudioPlaying, @@ -137,6 +140,11 @@ export const SimulatorControls: React.FC = ({ title: "Scenarios", onClick: handleClick(onScenarioPanel), }, + { + icon: , + title: "Load JSON", + onClick: handleClick(onJsonPanel), + }, { icon: , title: "Settings", diff --git a/src/schemas/physics.ts b/src/schemas/physics.ts new file mode 100644 index 0000000..e988607 --- /dev/null +++ b/src/schemas/physics.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; +import { Point } from "paper"; + +// Runtime types (these can't be validated with Zod since they're class instances) +export type Vector = InstanceType; +export type Force = Vector; + +// Zod schemas for runtime validation +export const WarpPointSchema = z.object({ + position: z.instanceof(Point), + effectiveMass: z.number(), +}); + +export const GravityPointSchema = z.object({ + position: z.instanceof(Point), + label: z.string(), + mass: z.number(), + id: z.string().optional(), +}); + +export const ParticleMechanicsSchema = z.object({ + position: z.instanceof(Point), + velocity: z.instanceof(Point), + force: z.instanceof(Point), + mass: z.number(), + elasticity: z.number(), + outgoingForceRatio: z.number().optional(), + frozen: z.boolean().optional(), +}); + +export const ParticleSchema = ParticleMechanicsSchema.extend({ + id: z.string(), + color: z.string().optional(), + size: z.number().optional(), + showVectors: z.boolean().optional(), +}); + +// Export types +export type WarpPoint = z.infer; +export type GravityPoint = z.infer; +export type ParticleMechanics = z.infer; +export type Particle = z.infer; diff --git a/src/schemas/scenario.ts b/src/schemas/scenario.ts new file mode 100644 index 0000000..cb360f6 --- /dev/null +++ b/src/schemas/scenario.ts @@ -0,0 +1,108 @@ +import { z } from "zod"; + +export const Point2DSchema = z.object({ + x: z.number(), + y: z.number(), +}); + +export const PathPointSchema = z.object({ + x: z.number(), + y: z.number(), + handleIn: Point2DSchema.optional(), + handleOut: Point2DSchema.optional(), +}); + +export const SerializableGravityPointSchema = z.object({ + x: z.number(), + y: z.number(), + label: z.string(), + mass: z.number(), + id: z.string().optional(), +}); + +export const SerializableParticleSchema = z.object({ + id: z.string(), + position: Point2DSchema, + velocity: Point2DSchema, + mass: z.number(), + elasticity: z.number(), + outgoingForceRatio: z.number().optional(), + size: z.number().optional(), + color: z.string().optional(), + showVectors: z.boolean().optional(), +}); + +export const SerializableSimulatorPathSchema = z.object({ + id: z.string(), + points: z.array(PathPointSchema), + closed: z.boolean(), + position: Point2DSchema, + label: z.string(), + mass: z.number(), + strokeColor: z.string().optional(), + fillColor: z.string().optional(), + strokeWidth: z.number().optional(), + opacity: z.number().optional(), +}); + +export const PhysicsSettingsSchema = z.object({ + NEW_PARTICLE_MASS: z.number().optional(), + NEW_PARTICLE_ELASTICITY: z.number().optional(), + FRICTION: z.number().optional(), + DELTA_TIME: z.number().optional(), + POINTER_MASS: z.number().optional(), + SHOW_VELOCITY_ARROWS: z.boolean().optional(), + SHOW_FORCE_ARROWS: z.boolean().optional(), + CONSTANT_FORCE: Point2DSchema.optional(), + SOLID_BOUNDARIES: z.boolean().optional(), + PARTICLES_EXERT_GRAVITY: z.boolean().optional(), + PARTICLE_TRAIL_LENGTH: z.number().optional(), + SHOW_GRAVITY_VISION: z.boolean().optional(), + GRAVITY_GRID_DENSITY: z.number().optional(), + SHOW_D3_GRAVITY_VISION: z.boolean().optional(), + GRAVITY_VISION_OPACITY: z.number().optional(), + GRAVITY_VISION_STROKE_OPACITY: z.number().optional(), + GRAVITY_VISION_STROKE_WIDTH: z.number().optional(), + GRAVITY_VISION_STROKE_COLOR: z.string().optional(), + GRAVITY_VISION_COLOR_SCHEME: z.string().optional(), + GRAVITY_VISION_INVERT_COLORS: z.boolean().optional(), + GRAVITY_VISION_GRID_SIZE: z.number().optional(), + GRAVITY_VISION_CONTOUR_LEVELS: z.number().optional(), + GRAVITY_VISION_THROTTLE_MS: z.number().optional(), + GRAVITY_VISION_TRANSITION_MS: z.number().optional(), + GRAVITY_VISION_STRENGTH: z.number().optional(), + GRAVITY_VISION_FALLOFF: z.number().optional(), + GRAVITY_VISION_MASS_THRESHOLD: z.number().optional(), + GRAVITY_VISION_BLUR: z.number().optional(), + MASTER_VOLUME: z.number().optional(), + AMBIENT_VOLUME: z.number().optional(), + PARTICLE_VOLUME: z.number().optional(), +}); + +export const ScenarioDataSchema = z.object({ + settings: PhysicsSettingsSchema, + gravityPoints: z.array(SerializableGravityPointSchema).optional(), + particles: z.array(SerializableParticleSchema).optional(), + paths: z.array(SerializableSimulatorPathSchema).optional(), +}); + +export const ScenarioSchema = z.object({ + id: z.string(), + name: z.string(), + description: z.string(), + data: ScenarioDataSchema, +}); + +// Export types derived from schemas +export type Point2D = z.infer; +export type PathPoint = z.infer; +export type SerializableGravityPoint = z.infer< + typeof SerializableGravityPointSchema +>; +export type SerializableParticle = z.infer; +export type SerializableSimulatorPath = z.infer< + typeof SerializableSimulatorPathSchema +>; +export type PhysicsSettings = z.infer; +export type ScenarioData = z.infer; +export type Scenario = z.infer; diff --git a/src/types/particle.ts b/src/types/particle.ts index db6130b..56203d3 100644 --- a/src/types/particle.ts +++ b/src/types/particle.ts @@ -1,37 +1,11 @@ import { Point } from "paper/dist/paper-core"; -import { Vector, Point2D } from "../utils/types/physics"; +import { type SerializableParticle } from "../schemas/scenario"; +import { type Particle } from "../schemas/physics"; -export interface ParticleMechanics { - position: Vector; - velocity: Vector; - force: Vector; - mass: number; - elasticity: number; - outgoingForceRatio?: number; - frozen?: boolean; -} +export { type Particle, type ParticleMechanics } from "../schemas/physics"; +export { type SerializableParticle } from "../schemas/scenario"; -export interface Particle extends ParticleMechanics { - id: string; - color?: string; - size?: number; - showVectors?: boolean; -} - -// Serializable versions -export interface SerializableParticle { - id: string; - position: Point2D; - velocity: Point2D; - mass: number; - elasticity: number; - outgoingForceRatio?: number; - size?: number; - color?: string; - showVectors?: boolean; -} - -// Conversion utilities +// Keep the conversion utilities export const toParticle = (p: SerializableParticle): Particle => ({ id: p.id, position: new Point(p.position.x, p.position.y), diff --git a/src/types/scenario.ts b/src/types/scenario.ts index 495cf6e..485126e 100644 --- a/src/types/scenario.ts +++ b/src/types/scenario.ts @@ -1,18 +1 @@ -import { SerializableGravityPoint } from "../utils/types/physics"; -import { SerializableParticle } from "./particle"; -import { PhysicsSettings } from "../constants/physics"; -import { SerializableSimulatorPath } from "../utils/types/path"; - -export interface ScenarioData { - settings: Partial; - gravityPoints?: Array; - particles?: Array; - paths?: Array; -} - -export interface Scenario { - id: string; - name: string; - description: string; - data: ScenarioData; -} +export { type Scenario, type ScenarioData } from "../schemas/scenario"; diff --git a/src/utils/types/path.ts b/src/utils/types/path.ts index 275227d..f4d525f 100644 --- a/src/utils/types/path.ts +++ b/src/utils/types/path.ts @@ -1,28 +1,14 @@ import { Path, Point, Segment } from "paper"; -import { Vector, SerializablePath } from "./physics"; +import { type Vector, type SerializablePath } from "./physics"; +import { + type PathPoint, + type SerializableSimulatorPath, +} from "../../schemas/scenario"; -// Represents a point in a path with optional curve data -export interface PathPoint { - x: number; - y: number; - handleIn?: { x: number; y: number }; // Control point for curves coming in - handleOut?: { x: number; y: number }; // Control point for curves going out -} - -// The serializable version of a SimulatorPath -export interface SerializableSimulatorPath { - id: string; - points: PathPoint[]; - closed: boolean; - position: { x: number; y: number }; // Center/reference point - label: string; - mass: number; - // Additional styling properties - strokeColor?: string; - fillColor?: string; - strokeWidth?: number; - opacity?: number; -} +export { + type PathPoint, + type SerializableSimulatorPath, +} from "../../schemas/scenario"; // The runtime version using Paper.js objects export interface SimulatorPath { diff --git a/src/utils/types/physics.ts b/src/utils/types/physics.ts index c493da5..a8013ad 100644 --- a/src/utils/types/physics.ts +++ b/src/utils/types/physics.ts @@ -1,34 +1,21 @@ import { Point, Path } from "paper"; -// Basic vector type for x,y coordinates (JSON serializable) -export interface Point2D { - x: number; - y: number; -} -export interface WarpPoint { - position: Vector; - effectiveMass: number; -} -// Paper.js Point type - use the actual instance type export type Vector = InstanceType; export type SerializablePath = InstanceType; -export type Force = Vector; -export interface GravityPoint { - position: Vector; - label: string; - mass: number; - id?: string; -} +export { + type Point2D, + type SerializableGravityPoint, +} from "../../schemas/scenario"; +export { + type WarpPoint, + type GravityPoint, + type Force, +} from "../../schemas/physics"; -// Serializable version of GravityPoint -export interface SerializableGravityPoint { - x: number; - y: number; - label: string; - mass: number; - id?: string; -} +// Keep the conversion utilities +import { type SerializableGravityPoint } from "../../schemas/scenario"; +import { type GravityPoint } from "../../schemas/physics"; export const toGravityPoint = (p: SerializableGravityPoint): GravityPoint => ({ position: new Point(p.x, p.y),