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