"""File export: SVG and DXF for 2D geometry, STEP and STL for optional solids. Every function is pure output: it takes geometry plus a path and writes a file. No geometry is generated here beyond grouping already-built shapes. 3MF lives next door in :mod:`chess2d.bambu`, which also lays the pieces out on a build plate; :func:`generate_all` can include one via ``with_3mf``. """ from __future__ import annotations from pathlib import Path from build123d import ( Color, Compound, ExportDXF, ExportSVG, Pos, Sketch, export_step, export_stl, extrude, ) from .assembly import ChessComposition, make_initial_position from .board import BoardGeometry, make_board from .parameters import ( BLACK_FILL_COLOR, DARK_SQUARE_COLOR, LIGHT_SQUARE_COLOR, SQUARE_SIZE, WHITE_FILL_COLOR, ChessStyle, FigureMode, PieceStyle, PieceType, ) from .pieces import make_piece, make_piece_solid def _color(rgb: tuple[float, float, float]) -> Color: return Color(*rgb) def _ensure_parent(path: str | Path) -> Path: p = Path(path) p.parent.mkdir(parents=True, exist_ok=True) return p # -------------------------------------------------------------------------- # SVG # -------------------------------------------------------------------------- def export_piece_svg( piece_type: PieceType, path: str | Path, scale: float = 1.0, fill: tuple[float, float, float] = WHITE_FILL_COLOR, mode: FigureMode = FigureMode.TWO_SIDED, square_size: float = SQUARE_SIZE, style: PieceStyle = PieceStyle.STAUNTON, ) -> Path: """Write a single filled piece silhouette as an SVG.""" out = _ensure_parent(path) sketch = make_piece(piece_type, scale=scale, mode=mode, square_size=square_size, style=style) exporter = ExportSVG(fit_to_stroke=True) exporter.add_layer("fill", fill_color=_color(fill), line_color=Color(0.15, 0.15, 0.15)) exporter.add_shape(sketch, layer="fill") exporter.write(str(out)) return out def export_board_svg(board: BoardGeometry, path: str | Path) -> Path: """Write the board (light + dark squares, border) as a layered SVG.""" out = _ensure_parent(path) exporter = ExportSVG(fit_to_stroke=True) exporter.add_layer( "light", fill_color=_color(LIGHT_SQUARE_COLOR), line_color=_color(DARK_SQUARE_COLOR), ) exporter.add_layer( "dark", fill_color=_color(DARK_SQUARE_COLOR), line_color=_color(DARK_SQUARE_COLOR), ) exporter.add_shape(board.light_squares, layer="light") exporter.add_shape(board.dark_squares, layer="dark") if board.border is not None: exporter.add_layer("border", fill_color=None, line_color=Color(0.2, 0.2, 0.2)) exporter.add_shape(board.border, layer="border") exporter.write(str(out)) return out def export_composition_svg(composition: ChessComposition, path: str | Path) -> Path: """Write the full board + both piece layers as a single layered SVG.""" out = _ensure_parent(path) exporter = ExportSVG(fit_to_stroke=True) exporter.add_layer( "light", fill_color=_color(LIGHT_SQUARE_COLOR), line_color=_color(DARK_SQUARE_COLOR), ) exporter.add_layer( "dark", fill_color=_color(DARK_SQUARE_COLOR), line_color=_color(DARK_SQUARE_COLOR), ) exporter.add_layer( "white_pieces", fill_color=_color(WHITE_FILL_COLOR), line_color=Color(0.2, 0.2, 0.2), ) exporter.add_layer( "black_pieces", fill_color=_color(BLACK_FILL_COLOR), line_color=Color(0.1, 0.1, 0.1), ) exporter.add_shape(composition.board.light_squares, layer="light") exporter.add_shape(composition.board.dark_squares, layer="dark") exporter.add_shape(composition.white_pieces, layer="white_pieces") exporter.add_shape(composition.black_pieces, layer="black_pieces") exporter.write(str(out)) return out # -------------------------------------------------------------------------- # DXF # -------------------------------------------------------------------------- def export_dxf(shapes: dict[str, Sketch], path: str | Path) -> Path: """Write a set of named shape layers to a single DXF.""" out = _ensure_parent(path) exporter = ExportDXF() for name, shape in shapes.items(): exporter.add_layer(name) exporter.add_shape(shape, layer=name) exporter.write(str(out)) return out # -------------------------------------------------------------------------- # STEP / STL (optional solids) # -------------------------------------------------------------------------- def export_piece_step( piece_type: PieceType, path: str | Path, thickness: float, scale: float = 1.0, mode: FigureMode = FigureMode.TWO_SIDED, square_size: float = SQUARE_SIZE, style: PieceStyle = PieceStyle.STAUNTON, ) -> Path: out = _ensure_parent(path) solid = make_piece_solid( piece_type, thickness=thickness, scale=scale, mode=mode, square_size=square_size, style=style, ) export_step(solid, str(out)) return out def export_piece_stl( piece_type: PieceType, path: str | Path, thickness: float, scale: float = 1.0, mode: FigureMode = FigureMode.TWO_SIDED, square_size: float = SQUARE_SIZE, style: PieceStyle = PieceStyle.STAUNTON, ) -> Path: out = _ensure_parent(path) solid = make_piece_solid( piece_type, thickness=thickness, scale=scale, mode=mode, square_size=square_size, style=style, ) export_stl(solid, str(out)) return out def export_flat_pieces_step( path: str | Path, thickness: float, scale: float = 1.0, mode: FigureMode = FigureMode.TWO_SIDED, square_size: float = SQUARE_SIZE, style: PieceStyle = PieceStyle.STAUNTON, ) -> Path: """All six flat-piece solids laid out in a row, as one STEP file.""" out = _ensure_parent(path) solids = [] for i, pt in enumerate(PieceType): solid = make_piece_solid( pt, thickness=thickness, scale=scale, mode=mode, square_size=square_size, style=style, ) solids.append(Pos((i - 2.5) * square_size * 0.95, 0, 0) * solid) compound = Compound(children=solids) export_step(compound, str(out)) return out # -------------------------------------------------------------------------- # Batch driver # -------------------------------------------------------------------------- def generate_all( output_dir: str | Path = "output", style: ChessStyle | None = None, with_solids: bool = True, with_3mf: bool = False, ) -> list[Path]: """Regenerate every deliverable under ``output_dir``. Returns written paths. ``with_3mf`` adds a meshed 3MF of the six pieces, ready to open in Bambu Studio (or any other slicer) -- see :mod:`chess2d.bambu` for print plates holding a whole set. """ if style is None: style = ChessStyle() root = Path(output_dir) written: list[Path] = [] board = make_board(square_size=style.square_size, border_width=style.border_width) composition = make_initial_position(style) # --- SVG --- svg_dir = root / "svg" written.append(export_board_svg(board, svg_dir / "board.svg")) written.extend( export_piece_svg( pt, svg_dir / f"{pt.value}.svg", style.piece_scale, mode=style.figure_mode, square_size=style.square_size, style=style.piece_style, ) for pt in PieceType ) written.append(export_composition_svg(composition, svg_dir / "initial_position.svg")) # --- DXF --- dxf_dir = root / "dxf" written.append( export_dxf({"board": board.light_squares + board.dark_squares}, dxf_dir / "board.dxf"), ) # Space the pieces out so they do not overlap in the DXF. spaced = { pt.value: Pos((i - 2.5) * style.square_size * 0.95, 0) * make_piece( pt, scale=style.piece_scale, mode=style.figure_mode, square_size=style.square_size, style=style.piece_style, ) for i, pt in enumerate(PieceType) } written.append(export_dxf(spaced, dxf_dir / "pieces.dxf")) written.append( export_dxf( { "light": composition.board.light_squares, "dark": composition.board.dark_squares, "white_pieces": composition.white_pieces, "black_pieces": composition.black_pieces, }, dxf_dir / "initial_position.dxf", ), ) if with_solids: step_dir = root / "step" stl_dir = root / "stl" board_solid_path = step_dir / "board.step" _ensure_parent(board_solid_path) export_step( extrude(board.light_squares + board.dark_squares, amount=style.board_thickness), str(board_solid_path), ) written.append(board_solid_path) written.append( export_flat_pieces_step( step_dir / "flat_pieces.step", style.piece_thickness, style.piece_scale, mode=style.figure_mode, square_size=style.square_size, style=style.piece_style, ), ) written.extend( export_piece_stl( pt, stl_dir / f"{pt.value}.stl", style.piece_thickness, style.piece_scale, mode=style.figure_mode, square_size=style.square_size, style=style.piece_style, ) for pt in PieceType ) if with_3mf: # Imported here: the 3MF writer is only needed on this branch, and the # module reaches back into this one's siblings for the plate layout. from .bambu import export_pieces_3mf # noqa: PLC0415 written.append( export_pieces_3mf( root / "3mf" / "pieces.3mf", style.piece_thickness, style.piece_scale, mode=style.figure_mode, square_size=style.square_size, style=style.piece_style, ), ) return written