tweak the fit of the parts
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Combine aligned lid meshes into one slicer-friendly multipart 3MF."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
|
||||
CORE = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
|
||||
ET.register_namespace("", CORE)
|
||||
|
||||
CONTENT_TYPES_XML = b'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>
|
||||
</Types>
|
||||
'''
|
||||
|
||||
RELATIONSHIPS_XML = b'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>
|
||||
</Relationships>
|
||||
'''
|
||||
|
||||
|
||||
def load_mesh(path: Path) -> ET.Element:
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
root = ET.fromstring(archive.read("3D/3dmodel.model"))
|
||||
mesh = root.find(f".//{{{CORE}}}mesh")
|
||||
if mesh is None:
|
||||
raise ValueError(f"{path} does not contain a 3MF mesh")
|
||||
return copy.deepcopy(mesh)
|
||||
|
||||
|
||||
def model_xml(base: Path, accent: Path, title: str) -> bytes:
|
||||
root = ET.Element(
|
||||
f"{{{CORE}}}model",
|
||||
{
|
||||
"unit": "millimeter",
|
||||
"{http://www.w3.org/XML/1998/namespace}lang": "en-US",
|
||||
},
|
||||
)
|
||||
ET.SubElement(root, f"{{{CORE}}}metadata", {"name": "Title"}).text = title
|
||||
ET.SubElement(root, f"{{{CORE}}}metadata", {"name": "Generator"}).text = (
|
||||
"feather-case-wizard"
|
||||
)
|
||||
resources = ET.SubElement(root, f"{{{CORE}}}resources")
|
||||
materials = ET.SubElement(resources, f"{{{CORE}}}basematerials", {"id": "1"})
|
||||
ET.SubElement(
|
||||
materials, f"{{{CORE}}}base", {"name": "Lid base", "displaycolor": "#D1D5DB"}
|
||||
)
|
||||
ET.SubElement(
|
||||
materials,
|
||||
f"{{{CORE}}}base",
|
||||
{"name": "Accent inlays", "displaycolor": "#F97316"},
|
||||
)
|
||||
|
||||
for object_id, name, source in (
|
||||
(2, "Lid base — Filament 1", base),
|
||||
(3, "Accent inlays — Filament 2", accent),
|
||||
):
|
||||
obj = ET.SubElement(
|
||||
resources,
|
||||
f"{{{CORE}}}object",
|
||||
{
|
||||
"id": str(object_id),
|
||||
"name": name,
|
||||
"type": "model",
|
||||
"pid": "1",
|
||||
"pindex": str(object_id - 2),
|
||||
},
|
||||
)
|
||||
obj.append(load_mesh(source))
|
||||
|
||||
assembly = ET.SubElement(
|
||||
resources, f"{{{CORE}}}object", {"id": "4", "name": title, "type": "model"}
|
||||
)
|
||||
components = ET.SubElement(assembly, f"{{{CORE}}}components")
|
||||
ET.SubElement(components, f"{{{CORE}}}component", {"objectid": "2"})
|
||||
ET.SubElement(components, f"{{{CORE}}}component", {"objectid": "3"})
|
||||
build = ET.SubElement(root, f"{{{CORE}}}build")
|
||||
ET.SubElement(build, f"{{{CORE}}}item", {"objectid": "4"})
|
||||
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||
|
||||
|
||||
def bambu_model_settings_xml(title: str) -> bytes:
|
||||
root = ET.Element("config")
|
||||
obj = ET.SubElement(root, "object", {"id": "4"})
|
||||
ET.SubElement(obj, "metadata", {"key": "name", "value": title})
|
||||
for part_id, name, extruder in (
|
||||
(2, "Lid base — Filament 1", 1),
|
||||
(3, "Accent inlays — Filament 2", 2),
|
||||
):
|
||||
part = ET.SubElement(obj, "part", {"id": str(part_id), "subtype": "normal_part"})
|
||||
ET.SubElement(part, "metadata", {"key": "name", "value": name})
|
||||
ET.SubElement(part, "metadata", {"key": "extruder", "value": str(extruder)})
|
||||
ET.SubElement(root, "assemble")
|
||||
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||
|
||||
|
||||
def write_entry(archive: zipfile.ZipFile, name: str, data: bytes) -> None:
|
||||
info = zipfile.ZipInfo(name)
|
||||
info.create_system = 3
|
||||
info.external_attr = 0o100644 << 16
|
||||
info.compress_type = zipfile.ZIP_DEFLATED
|
||||
archive.writestr(info, data)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("base", type=Path)
|
||||
parser.add_argument("accent", type=Path)
|
||||
parser.add_argument("output", type=Path)
|
||||
parser.add_argument("--title", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(args.output, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||
write_entry(archive, "[Content_Types].xml", CONTENT_TYPES_XML)
|
||||
write_entry(archive, "_rels/.rels", RELATIONSHIPS_XML)
|
||||
write_entry(
|
||||
archive,
|
||||
"3D/3dmodel.model",
|
||||
model_xml(args.base, args.accent, args.title),
|
||||
)
|
||||
write_entry(
|
||||
archive,
|
||||
"Metadata/model_settings.config",
|
||||
bambu_model_settings_xml(args.title),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Render aligned lid-base and accent STLs with distinct materials."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import math
|
||||
|
||||
import bpy
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
|
||||
def import_mesh(path, name, color):
|
||||
bpy.ops.wm.stl_import(filepath=os.path.abspath(path))
|
||||
model = bpy.context.selected_objects[0]
|
||||
model.name = name
|
||||
material = bpy.data.materials.new(f"{name} material")
|
||||
material.diffuse_color = color
|
||||
material.metallic = 0.0
|
||||
material.roughness = 0.45
|
||||
model.data.materials.append(material)
|
||||
return model
|
||||
|
||||
|
||||
def look_at(camera, target):
|
||||
direction = Vector(target) - camera.location
|
||||
camera.rotation_euler = direction.to_track_quat("-Z", "Y").to_euler()
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[sys.argv.index("--") + 1 :]
|
||||
if len(args) not in (4, 5):
|
||||
raise SystemExit(
|
||||
"expected: lid-base.stl lid-accent.stl output-directory basename [orientation]"
|
||||
)
|
||||
base_path, accent_path, output_dir, basename = args[:4]
|
||||
orientation = args[4] if len(args) == 5 else "usb_left"
|
||||
angles = {"usb_left": 0, "usb_front": 90, "usb_right": 180, "usb_rear": 270}
|
||||
if orientation not in angles:
|
||||
raise SystemExit(f"unknown lid orientation: {orientation}")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
models = [
|
||||
import_mesh(base_path, "Lid base", (0.16, 0.42, 0.72, 1.0)),
|
||||
import_mesh(accent_path, "Accent inlays", (0.95, 0.42, 0.08, 1.0)),
|
||||
]
|
||||
# Lid exports are print-oriented with the decorated exterior face down.
|
||||
# Turn both aligned parts over and present the configured USB edge in its
|
||||
# named position so previews inspect the finished face as the user will.
|
||||
for model in models:
|
||||
model.matrix_world = (
|
||||
Matrix.Rotation(math.radians(angles[orientation]), 4, "Z")
|
||||
@ Matrix.Rotation(math.pi, 4, "X")
|
||||
)
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
corners = [obj.matrix_world @ Vector(corner) for obj in models for corner in obj.bound_box]
|
||||
mins = Vector([min(v[i] for v in corners) for i in range(3)])
|
||||
maxs = Vector([max(v[i] for v in corners) for i in range(3)])
|
||||
center = (mins + maxs) / 2
|
||||
span = max(maxs - mins)
|
||||
|
||||
world = bpy.data.worlds.new("Inspection world")
|
||||
world.use_nodes = True
|
||||
world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.035, 0.04, 0.05, 1)
|
||||
world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.5
|
||||
bpy.context.scene.world = world
|
||||
|
||||
camera_data = bpy.data.cameras.new("Camera")
|
||||
camera = bpy.data.objects.new("Camera", camera_data)
|
||||
bpy.context.collection.objects.link(camera)
|
||||
bpy.context.scene.camera = camera
|
||||
camera.data.type = "ORTHO"
|
||||
camera.data.ortho_scale = span * 1.35
|
||||
|
||||
key_data = bpy.data.lights.new("Key", type="AREA")
|
||||
key_data.energy = 900
|
||||
key_data.size = span
|
||||
key = bpy.data.objects.new("Key", key_data)
|
||||
key.location = center + Vector((span, -span, span * 1.5))
|
||||
bpy.context.collection.objects.link(key)
|
||||
|
||||
scene = bpy.context.scene
|
||||
scene.render.engine = "BLENDER_WORKBENCH"
|
||||
scene.render.resolution_x = 800
|
||||
scene.render.resolution_y = 600
|
||||
scene.render.resolution_percentage = 100
|
||||
scene.render.image_settings.file_format = "PNG"
|
||||
scene.display.shading.light = "STUDIO"
|
||||
scene.display.shading.color_type = "MATERIAL"
|
||||
scene.display.shading.show_shadows = True
|
||||
scene.display.shading.show_cavity = True
|
||||
scene.display.shading.cavity_type = "BOTH"
|
||||
|
||||
for view, direction in {"iso": (1, -1, 0.8), "top": (0, 0, 1)}.items():
|
||||
camera.location = center + Vector(direction).normalized() * span * 2.2
|
||||
look_at(camera, center)
|
||||
scene.render.filepath = os.path.join(output_dir, f"{basename}_{view}.png")
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user