Initial scaffold for artifact-editor project
This commit is contained in:
commit
6ccdd84835
|
|
@ -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
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
"""Artifact Editor - AI-enhanced editor for diagrams, sketches, and 3D models."""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
|
@ -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())
|
||||||
|
|
@ -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())
|
||||||
|
|
@ -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
|
||||||
|
"""
|
||||||
|
|
@ -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)
|
||||||
|
"""
|
||||||
Loading…
Reference in New Issue