commit 6ccdd8483531eec74e44cbc4a7b433b28c48a1e6 Author: rob Date: Thu Dec 18 22:06:34 2025 -0400 Initial scaffold for artifact-editor project diff --git a/README.md b/README.md new file mode 100644 index 0000000..14bd35f --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +# Artifact Editor + +**AI-enhanced editor for creating diagrams, sketches, 3D models, and other artifacts from code.** + +A standalone editor that can be launched from any application to create visual artifacts. Designed to integrate with discussion tools, documentation systems, and IDEs. + +## Vision + +Enable users to create rich visual artifacts through multiple input methods: +- **Code editing** - Write Mermaid, PlantUML, OpenSCAD, SVG directly +- **Graphical interface** - Drag-and-drop, visual editing +- **Voice input** - Describe what you want, AI generates the code +- **AI assistance** - "Add a database to this diagram", "Make the cube hollow" + +## Supported Artifact Types + +| Type | Description | Output Formats | +|------|-------------|----------------| +| `mermaid` | Flowcharts, sequence, ER, state diagrams | SVG, PNG, PDF | +| `plantuml` | UML diagrams (class, sequence, etc.) | SVG, PNG | +| `openscad` | 3D parametric CAD models | STL, PNG, SVG | +| `solidpython` | Python-based 3D CAD (via SolidPython) | STL, PNG, SCAD | +| `svg` | Raw SVG vector graphics | SVG | +| `asciiart` | ASCII/Unicode box drawing | TXT, PNG | +| `excalidraw` | Hand-drawn style diagrams | SVG, PNG | +| `code` | Syntax-highlighted code snippets | SVG, PNG, HTML | + +## Installation + +```bash +pip install artifact-editor + +# Optional renderers +npm install -g @mermaid-js/mermaid-cli # For Mermaid +sudo apt install openscad # For 3D CAD +pip install solidpython2 # For SolidPython +``` + +## Usage + +### As a Standalone Editor + +```bash +# Launch editor for a new Mermaid diagram +artifact-editor --type mermaid --output diagram.svg + +# Edit an existing artifact +artifact-editor --type openscad --output model.stl --initial-file model.scad + +# Headless AI generation +artifact-editor --type mermaid --output flow.svg --headless \ + --prompt "Create a flowchart for user authentication" +``` + +### Integration with Other Applications + +The editor follows a simple contract for integration: + +```bash +# Launch +artifact-editor --type TYPE --output /path/to/output.svg [--project NAME] + +# Exit behavior: +# - Exit code 0: Saved successfully +# stdout: "ARTIFACT_SAVED:/path/to/output.svg" +# +# - Exit code 1: Cancelled by user +# (no output) +# +# - Exit code 2: Error +# stderr: error message +``` + +Example integration (Python): +```python +import subprocess + +result = subprocess.run([ + "artifact-editor", + "--type", "mermaid", + "--output", "/tmp/diagram.svg", + "--project", "my-project" +], capture_output=True, text=True) + +if result.returncode == 0: + # Parse output to get file path + for line in result.stdout.split('\n'): + if line.startswith("ARTIFACT_SAVED:"): + artifact_path = line.split(":", 1)[1] + print(f"Created: {artifact_path}") +``` + +## Architecture + +``` +artifact-editor/ +├── src/artifact_editor/ +│ ├── cli.py # CLI entry point +│ ├── editor.py # TUI editor (urwid-based) +│ ├── ai_assist.py # AI generation/modification +│ ├── renderers/ # Convert source to visual output +│ │ ├── mermaid.py +│ │ ├── plantuml.py +│ │ ├── openscad.py +│ │ ├── svg.py +│ │ └── ... +│ └── ui/ # UI components +│ ├── code_editor.py +│ ├── preview.py +│ └── ai_panel.py +``` + +## Roadmap + +- [ ] Basic TUI with code editor and preview +- [ ] Mermaid renderer +- [ ] OpenSCAD/SolidPython renderer +- [ ] AI assistance integration (via SmartTools) +- [ ] Voice input (via SmartTools dictate) +- [ ] PlantUML renderer +- [ ] SVG direct editing +- [ ] Excalidraw-style sketching +- [ ] Plugin system for custom renderers + +## Integration Goals + +This editor is designed to integrate with: +- **orchestrated-discussions** - Add artifacts to discussion comments +- **Documentation systems** - Embed diagrams in docs +- **IDEs** - Quick diagram creation from code +- **Chat applications** - Share visual explanations + +## License + +MIT diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8c4d14e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,65 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "artifact-editor" +version = "0.1.0" +description = "AI-enhanced editor for creating diagrams, sketches, 3D models, and other artifacts from code" +readme = "README.md" +license = {text = "MIT"} +requires-python = ">=3.10" +authors = [ + {name = "Rob"} +] +keywords = ["diagrams", "uml", "mermaid", "openscad", "3d", "artifacts", "editor", "ai"] +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Multimedia :: Graphics", + "Topic :: Software Development :: Documentation", +] +dependencies = [ + "PyYAML>=6.0", +] + +[project.optional-dependencies] +tui = [ + "urwid>=2.1.0", +] +mermaid = [ + # mermaid-py or use CLI +] +openscad = [ + "solidpython2>=2.0.0", +] +all = [ + "urwid>=2.1.0", + "solidpython2>=2.0.0", +] +dev = [ + "pytest>=7.0", + "pytest-cov>=4.0", + "urwid>=2.1.0", +] + +[project.scripts] +artifact-editor = "artifact_editor.cli:main" + +[project.urls] +Homepage = "https://gitea.brrd.tech/rob/artifact-editor" +Repository = "https://gitea.brrd.tech/rob/artifact-editor.git" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] diff --git a/src/artifact_editor/__init__.py b/src/artifact_editor/__init__.py new file mode 100644 index 0000000..305ff2b --- /dev/null +++ b/src/artifact_editor/__init__.py @@ -0,0 +1,3 @@ +"""Artifact Editor - AI-enhanced editor for diagrams, sketches, and 3D models.""" + +__version__ = "0.1.0" diff --git a/src/artifact_editor/cli.py b/src/artifact_editor/cli.py new file mode 100644 index 0000000..ac91082 --- /dev/null +++ b/src/artifact_editor/cli.py @@ -0,0 +1,132 @@ +"""CLI entry point for Artifact Editor.""" + +import argparse +import sys +from pathlib import Path + +from . import __version__ + + +# Supported artifact types +ARTIFACT_TYPES = [ + "mermaid", # Flowcharts, sequence diagrams, ER diagrams + "plantuml", # UML diagrams + "svg", # Raw SVG graphics + "openscad", # 3D CAD (OpenSCAD/SolidPython) + "asciiart", # ASCII/box drawing diagrams + "code", # Syntax-highlighted code snippets + "excalidraw", # Hand-drawn style diagrams (JSON) +] + + +def main(): + """Main CLI entry point.""" + parser = argparse.ArgumentParser( + prog="artifact-editor", + description="AI-enhanced editor for creating diagrams, sketches, and 3D models" + ) + parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}") + + parser.add_argument( + "-t", "--type", + choices=ARTIFACT_TYPES, + default="mermaid", + help=f"Artifact type: {', '.join(ARTIFACT_TYPES)}" + ) + parser.add_argument( + "-o", "--output", + required=True, + help="Output file path for the artifact" + ) + parser.add_argument( + "-p", "--project", + default="", + help="Project name/context for AI assistance" + ) + parser.add_argument( + "-i", "--initial", + default="", + help="Initial content to pre-populate editor" + ) + parser.add_argument( + "--initial-file", + help="File containing initial content" + ) + parser.add_argument( + "--headless", + action="store_true", + help="Run without UI (for AI-only generation)" + ) + parser.add_argument( + "--prompt", + help="AI prompt for headless generation" + ) + + args = parser.parse_args() + + # Load initial content from file if specified + initial_content = args.initial + if args.initial_file: + initial_path = Path(args.initial_file) + if initial_path.exists(): + initial_content = initial_path.read_text() + else: + print(f"Error: Initial file not found: {args.initial_file}", file=sys.stderr) + return 1 + + output_path = Path(args.output).expanduser().resolve() + + if args.headless: + # Headless mode: generate via AI without UI + if not args.prompt: + print("Error: --prompt required for headless mode", file=sys.stderr) + return 1 + return run_headless( + artifact_type=args.type, + output_path=output_path, + prompt=args.prompt, + project=args.project, + initial=initial_content + ) + else: + # Interactive mode: launch editor UI + return run_editor( + artifact_type=args.type, + output_path=output_path, + project=args.project, + initial=initial_content + ) + + +def run_editor(artifact_type: str, output_path: Path, project: str, initial: str) -> int: + """Launch the interactive editor UI.""" + # TODO: Implement TUI editor + print(f"[TODO] Launch editor for {artifact_type}") + print(f" Output: {output_path}") + print(f" Project: {project or '(none)'}") + print(f" Initial content: {len(initial)} chars") + + # Placeholder: just write initial content or template + if initial: + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(initial) + print(f"ARTIFACT_SAVED:{output_path}") + return 0 + + print("Error: Editor UI not yet implemented", file=sys.stderr) + return 1 + + +def run_headless(artifact_type: str, output_path: Path, prompt: str, project: str, initial: str) -> int: + """Generate artifact via AI without UI.""" + # TODO: Implement AI generation + print(f"[TODO] Headless generation for {artifact_type}") + print(f" Prompt: {prompt}") + print(f" Output: {output_path}") + + print("Error: Headless generation not yet implemented", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/artifact_editor/renderers/__init__.py b/src/artifact_editor/renderers/__init__.py new file mode 100644 index 0000000..b74c214 --- /dev/null +++ b/src/artifact_editor/renderers/__init__.py @@ -0,0 +1,65 @@ +"""Artifact renderers - convert source code to visual output.""" + +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Optional + + +class Renderer(ABC): + """Base class for artifact renderers.""" + + name: str = "base" + extensions: list[str] = [] + + @abstractmethod + def render(self, source: str, output_path: Path) -> bool: + """ + Render source code to output file. + + Args: + source: The source code/markup + output_path: Where to save the rendered output + + Returns: + True if successful, False otherwise + """ + pass + + @abstractmethod + def validate(self, source: str) -> tuple[bool, Optional[str]]: + """ + Validate source code syntax. + + Args: + source: The source code/markup to validate + + Returns: + (is_valid, error_message) - error_message is None if valid + """ + pass + + def get_template(self) -> str: + """Return a starter template for this artifact type.""" + return "" + + +# Registry of available renderers +_renderers: dict[str, type[Renderer]] = {} + + +def register_renderer(renderer_class: type[Renderer]) -> type[Renderer]: + """Decorator to register a renderer.""" + _renderers[renderer_class.name] = renderer_class + return renderer_class + + +def get_renderer(name: str) -> Optional[Renderer]: + """Get a renderer instance by name.""" + if name in _renderers: + return _renderers[name]() + return None + + +def list_renderers() -> list[str]: + """List available renderer names.""" + return list(_renderers.keys()) diff --git a/src/artifact_editor/renderers/mermaid.py b/src/artifact_editor/renderers/mermaid.py new file mode 100644 index 0000000..65d7666 --- /dev/null +++ b/src/artifact_editor/renderers/mermaid.py @@ -0,0 +1,76 @@ +"""Mermaid diagram renderer.""" + +import subprocess +import shutil +from pathlib import Path +from typing import Optional + +from . import Renderer, register_renderer + + +@register_renderer +class MermaidRenderer(Renderer): + """Render Mermaid diagrams to SVG/PNG.""" + + name = "mermaid" + extensions = [".svg", ".png", ".pdf"] + + def __init__(self): + self.cli_path = shutil.which("mmdc") # mermaid-cli + + def render(self, source: str, output_path: Path) -> bool: + """Render Mermaid source to output file.""" + if not self.cli_path: + print("Error: mermaid-cli (mmdc) not found. Install with: npm install -g @mermaid-js/mermaid-cli") + return False + + # Write source to temp file + import tempfile + with tempfile.NamedTemporaryFile(mode='w', suffix='.mmd', delete=False) as f: + f.write(source) + temp_input = f.name + + try: + result = subprocess.run( + [self.cli_path, "-i", temp_input, "-o", str(output_path)], + capture_output=True, + text=True + ) + + if result.returncode != 0: + print(f"Mermaid error: {result.stderr}") + return False + + return output_path.exists() + + finally: + Path(temp_input).unlink(missing_ok=True) + + def validate(self, source: str) -> tuple[bool, Optional[str]]: + """Validate Mermaid syntax.""" + # Basic validation - check for diagram type declaration + source = source.strip() + + valid_starts = [ + "graph ", "graph\n", + "flowchart ", "flowchart\n", + "sequenceDiagram", "classDiagram", "stateDiagram", + "erDiagram", "gantt", "pie", "journey", + "gitGraph", "mindmap", "timeline", + ] + + for start in valid_starts: + if source.startswith(start): + return True, None + + return False, "Unknown diagram type. Start with: graph, flowchart, sequenceDiagram, classDiagram, etc." + + def get_template(self) -> str: + """Return a starter Mermaid template.""" + return """graph TD + A[Start] --> B{Decision} + B -->|Yes| C[Action 1] + B -->|No| D[Action 2] + C --> E[End] + D --> E +""" diff --git a/src/artifact_editor/renderers/openscad.py b/src/artifact_editor/renderers/openscad.py new file mode 100644 index 0000000..7f9fead --- /dev/null +++ b/src/artifact_editor/renderers/openscad.py @@ -0,0 +1,177 @@ +"""OpenSCAD/SolidPython 3D CAD renderer.""" + +import subprocess +import shutil +from pathlib import Path +from typing import Optional + +from . import Renderer, register_renderer + + +@register_renderer +class OpenSCADRenderer(Renderer): + """Render OpenSCAD or SolidPython to 3D models.""" + + name = "openscad" + extensions = [".stl", ".png", ".scad", ".svg"] + + def __init__(self): + self.openscad_path = shutil.which("openscad") + + def render(self, source: str, output_path: Path) -> bool: + """Render OpenSCAD source to output file.""" + if not self.openscad_path: + print("Error: openscad not found. Install with: sudo apt install openscad") + return False + + # Detect if source is SolidPython (Python) or native OpenSCAD + is_solidpython = "from solid" in source or "import solid" in source + + if is_solidpython: + # Execute Python to generate OpenSCAD + scad_source = self._run_solidpython(source) + if scad_source is None: + return False + else: + scad_source = source + + # Write SCAD to temp file + import tempfile + with tempfile.NamedTemporaryFile(mode='w', suffix='.scad', delete=False) as f: + f.write(scad_source) + temp_input = f.name + + try: + suffix = output_path.suffix.lower() + + if suffix == ".scad": + # Just save the SCAD source + output_path.write_text(scad_source) + return True + + elif suffix == ".stl": + result = subprocess.run( + [self.openscad_path, "-o", str(output_path), temp_input], + capture_output=True, + text=True + ) + elif suffix == ".png": + result = subprocess.run( + [self.openscad_path, "-o", str(output_path), + "--autocenter", "--viewall", temp_input], + capture_output=True, + text=True + ) + elif suffix == ".svg": + result = subprocess.run( + [self.openscad_path, "-o", str(output_path), temp_input], + capture_output=True, + text=True + ) + else: + print(f"Unsupported output format: {suffix}") + return False + + if result.returncode != 0: + print(f"OpenSCAD error: {result.stderr}") + return False + + return output_path.exists() + + finally: + Path(temp_input).unlink(missing_ok=True) + + def _run_solidpython(self, source: str) -> Optional[str]: + """Execute SolidPython code and return OpenSCAD output.""" + import tempfile + import sys + + # Create temp Python file + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: + # Add code to output the SCAD + wrapper = f''' +{source} + +# Auto-added: output the result +if __name__ == "__main__": + import sys + # Find the main object (usually last assigned variable or 'result') + _locals = dict(locals()) + _obj = None + for _name in reversed(list(_locals.keys())): + _val = _locals[_name] + if hasattr(_val, 'render') or hasattr(_val, '_render'): + _obj = _val + break + if _obj is not None: + from solid2 import scad_render + print(scad_render(_obj)) + else: + print("// No SolidPython object found", file=sys.stderr) +''' + f.write(wrapper) + temp_py = f.name + + try: + result = subprocess.run( + [sys.executable, temp_py], + capture_output=True, + text=True + ) + + if result.returncode != 0: + print(f"SolidPython error: {result.stderr}") + return None + + return result.stdout + + finally: + Path(temp_py).unlink(missing_ok=True) + + def validate(self, source: str) -> tuple[bool, Optional[str]]: + """Validate OpenSCAD/SolidPython syntax.""" + # Basic validation + is_solidpython = "from solid" in source or "import solid" in source + + if is_solidpython: + # Try to parse as Python + import ast + try: + ast.parse(source) + return True, None + except SyntaxError as e: + return False, f"Python syntax error: {e}" + else: + # Basic OpenSCAD checks + if not any(kw in source for kw in ["cube", "sphere", "cylinder", "polyhedron", + "circle", "square", "polygon", + "linear_extrude", "rotate_extrude", + "union", "difference", "intersection", + "module", "function"]): + return False, "No OpenSCAD primitives or operations found" + return True, None + + def get_template(self) -> str: + """Return a starter OpenSCAD template.""" + return """// Simple OpenSCAD example +$fn = 32; // Smoothness + +difference() { + // Outer cube + cube([20, 20, 20], center=true); + + // Subtract a sphere + sphere(r=12); +} +""" + + def get_solidpython_template(self) -> str: + """Return a starter SolidPython template.""" + return """from solid2 import * + +# Set smoothness +set_global_fn(32) + +# Create a cube with a spherical hole +result = cube([20, 20, 20], center=True) - sphere(r=12) +"""