Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 169 additions & 3 deletions src/gflabel/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@
import logging
import os
import sys
import math
import ctypes
import copy
import warnings
from argparse import ArgumentParser
from pathlib import Path
from typing import Any, Sequence
from typing import Any, Sequence, Optional, Tuple, List
from xml.dom import minidom

import build123d as bd
import pint
Expand All @@ -36,6 +41,8 @@
extrude,
)

from build123d.geometry import TOLERANCE

from . import fragments
from .bases import LabelBase
from .bases.cullenect import CullenectBase
Expand Down Expand Up @@ -152,6 +159,63 @@ def base_name_to_subclass(name: str) -> type[LabelBase]:
)
return bases[name]

def write_slic3r_pe_model_config(volume_offsets: Sequence[Tuple[int, int]],
obj_name: str,
body_extruder: Optional[int] = None,
text_extruder: Optional[int] = None,
) -> str:
"""Create rendering of Slic3r_PE_model.config"""
doc = minidom.Document()
root = doc.createElement("config")
doc.appendChild(root)

obj = doc.createElement("object")
obj.setAttribute(attname="id", value="1")
obj.setAttribute(attname="instances_count", value=str(len(volume_offsets)))
root.appendChild(obj)

obj_metadata = doc.createElement("metadata")
for k, v in dict(type="object",
key="name",
value=str(obj_name)).items():
obj_metadata.setAttribute(attname=k, value=v)
obj.appendChild(obj_metadata)

for volume_id, vol_range in enumerate(volume_offsets):
volume = doc.createElement("volume")
volume.setAttribute(attname="firstid", value=str(vol_range[0]))
volume.setAttribute(attname="lastid", value=str(vol_range[1]))
volume_metadata_name = doc.createElement("metadata")
for k, v in dict(type="volume",
key="name",
value=f"{obj_name}_body" if volume_id == 0 else f"{obj_name}_text_{volume_id}").items():
volume_metadata_name.setAttribute(attname=k, value=v)
volume.appendChild(volume_metadata_name)
if volume_id == 0 and body_extruder is not None and isinstance(body_extruder, int):
vol_metadata_extruder = doc.createElement("metadata")
for k, v in dict(type="volume",
key="extruder",
value=str(body_extruder)).items():
vol_metadata_extruder.setAttribute(attname=k, value=v)
volume.appendChild(vol_metadata_extruder)
elif volume_id > 0 and text_extruder is not None and isinstance(text_extruder, int):
vol_metadata_extruder = doc.createElement("metadata")
for k, v in dict(type="volume",
key="extruder",
value=str(text_extruder)).items():
vol_metadata_extruder.setAttribute(attname=k, value=v)
volume.appendChild(vol_metadata_extruder)
obj.appendChild(volume)
return doc.toprettyxml(indent=" ", encoding="UTF-8").decode("utf-8")

def round_mesh_vertices(tolerance: float, verts: List[Tuple[float, float, float]]) -> List[Tuple[float, float, float]]:
"""round list of vertices to a given tolerance"""
digits = -int(round(math.log(tolerance, 10), 1))
ocp_mesh_vertices = [
(round(x, digits), round(y, digits), round(z, digits))
for x, y, z in verts
]
return ocp_mesh_vertices

def run(argv: list[str] | None = None):
# Handle the old way of specifying base
Expand Down Expand Up @@ -205,7 +269,28 @@ def run(argv: list[str] | None = None):
help="Disable the 'Overheight' system. This allows some symbols to oversize, meaning that the rest of the line will first shrink before they are shrunk.",
action="store_true",
)

parser.add_argument(
"--place-labeltext-on-plate",
dest="place_labeltext_on_plate",
help="reorient body such that embedded text is facing buildplate (down) [style=embedded only]",
action="store_true",
)
parser.add_argument(
"--3mf-text-extruder",
dest="threemf_text_extruder",
help="Which extruder to associate with text volumes in .3mf",
action="store",
type=int,
default=None,
)
parser.add_argument(
"--3mf-body-extruder",
dest="threemf_body_extruder",
help="Which extruder to associate with body volume in .3mf",
action="store",
type=int,
default=None,
)
parser.add_argument("labels", nargs="+", metavar="LABEL")
parser.add_argument(
"-d",
Expand Down Expand Up @@ -419,8 +504,9 @@ def run(argv: list[str] | None = None):
if args.style == LabelStyle.EMBEDDED:
# We want to make new volumes for the label, making it flush
embedded_label = extrude(label_sketch.sketch, amount=-args.depth)
embedded_label.label = "Label"
assembly = Compound([part.part, embedded_label])
if args.place_labeltext_on_plate:
assembly = assembly.rotate(axis=bd.Axis.X, angle=180)
else:
assembly = Compound(part.part)

Expand All @@ -444,6 +530,86 @@ def run(argv: list[str] | None = None):
logger.info(f"Writing SVG {output}")
exporter.add_shape(label_sketch.sketch, layer="Shapes")
exporter.write(output)
elif output.endswith(".3mf"):
exporter = bd.Mesher()

# -- Below, we'll recreate exporter.add_shape(), modifying some behavior throughout. --
volume_offsets: List[int] = [] # Capture how we'll partition parts in the object
unique_vertices: Sequence[Tuple[float, float, float]] = [] # No need to place duplicate vertices in 3dmodel.model
triangles_3mf: Sequence[bd.Lib3MF.Triangle] = [] # Create triangle point list

for b3d_shape in assembly.solids():
ocp_mesh_vertices, triangles = bd.Mesher._mesh_shape(ocp_mesh=copy.deepcopy(b3d_shape),
linear_deflection=0.001,
angular_deflection=0.1,
)
# Skip invalid meshes
if len(ocp_mesh_vertices) < 3 or not triangles:
warnings.warn(f"Degenerate shape {b3d_shape} - skipped",
stacklevel=2,
)
continue

# -- Below represents portions of exporter._create_3mf_mesh() --
# Round off the vertices to avoid vertices within tolerance being
# considered as different vertices
ocp_mesh_vertices = round_mesh_vertices(tolerance=TOLERANCE, verts=ocp_mesh_vertices)

# Create 3mf mesh inputs - Find any verts from ocp_mesh_vertices which don't exist in
# unique_vertices. Apppend the result to unique vertices.
unique_vertices.extend(list(set(ocp_mesh_vertices).difference(set(unique_vertices))))
vert_table = {
i: unique_vertices.index(pnt) for i, pnt in enumerate(ocp_mesh_vertices)
}

index_start = len(triangles_3mf) # Need to capture before modifying triangles_3mf
for vertex_indices in triangles:
mapped_indices = [
vert_table[i] for i in [vertex_indices[i] for i in range(3)]
]
# Remove degenerate triangles
if len(set(mapped_indices)) != 3:
continue
c_array = (ctypes.c_uint * 3)(*mapped_indices)
triangles_3mf.append(bd.Lib3MF.Triangle(c_array))

# Record start/end id of triangles for later use
volume_offsets.append((index_start, len(triangles_3mf) - 1))

# Create vertex list of 3MF positions
vertices_3mf: Sequence[bd.Lib3MF.Position] = []
for pnt in unique_vertices:
c_array = (ctypes.c_float * 3)(*pnt)
vertices_3mf.append(bd.Lib3MF.Position(c_array))
# mesh_3mf.AddVertex Should AddVertex be used to save memory?

# Build the mesh
mesh_3mf: bd.Lib3MF.MeshObject = exporter.model.AddMeshObject()
mesh_3mf.SetGeometry(vertices_3mf, triangles_3mf)

# Add the mesh properties
mesh_3mf.SetType(bd.Mesher._map_b3d_mesh_type_3mf[bd.MeshType.MODEL])
if b3d_shape.label:
mesh_3mf.SetName(b3d_shape.label)

# Add color
exporter._add_color(b3d_shape, mesh_3mf)

# Add mesh to model
exporter.meshes.append(mesh_3mf)
exporter.model.AddBuildItem(mesh_3mf, exporter.wrapper.GetIdentityTransform())

if args.threemf_body_extruder is not None or args.threemf_text_extruder is not None:
pe_model_config_text = write_slic3r_pe_model_config(obj_name=Path(output).stem,
volume_offsets=volume_offsets,
body_extruder=args.threemf_body_extruder,
text_extruder=args.threemf_text_extruder,
)
attachment = exporter.model.AddAttachment("Metadata/Slic3r_PE_model.config", "application/xml")
# ReadFromBuffer - "Read from Buffer into attachment file"
attachment.ReadFromBuffer(pe_model_config_text.encode("utf-8"))
logger.info(f"Writing 3MF {output}")
exporter.write(output)
else:
logger.error(f"Error: Do not understand output format '{args.output}'")

Expand Down