mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-09 14:55:03 -06:00
Merge branch 'master' of github.com:Ultimaker/Cura
This commit is contained in:
commit
a684e4def9
5 changed files with 954 additions and 7 deletions
|
@ -160,7 +160,12 @@ class StartSliceJob(Job):
|
|||
|
||||
obj = group_message.addRepeatedMessage("objects")
|
||||
obj.id = id(object)
|
||||
verts = numpy.array(mesh_data.getVertices())
|
||||
verts = mesh_data.getVertices()
|
||||
indices = mesh_data.getIndices()
|
||||
if indices is not None:
|
||||
verts = numpy.array([verts[vert_index] for face in indices for vert_index in face])
|
||||
else:
|
||||
verts = numpy.array(verts)
|
||||
|
||||
# Convert from Y up axes to Z up axes. Equals a 90 degree rotation.
|
||||
verts[:, [1, 2]] = verts[:, [2, 1]]
|
||||
|
|
|
@ -61,7 +61,7 @@ class SliceInfoJob(Job):
|
|||
# The data is only sent when the user in question gave permission to do so. All data is anonymous and
|
||||
# no model files are being sent (Just a SHA256 hash of the model).
|
||||
class SliceInfo(Extension):
|
||||
info_url = "http://stats.youmagine.com/curastats/slice"
|
||||
info_url = "https://stats.youmagine.com/curastats/slice"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
|
909
plugins/X3DReader/X3DReader.py
Normal file
909
plugins/X3DReader/X3DReader.py
Normal file
|
@ -0,0 +1,909 @@
|
|||
# Contributed by Seva Alekseyev <sevaa@nih.gov> with National Institutes of Health, 2016
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Mesh.MeshReader import MeshReader
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
from UM.Logger import Logger
|
||||
from UM.Math.Matrix import Matrix
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Job import Job
|
||||
from math import pi, sin, cos, sqrt
|
||||
import numpy
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
# TODO: preserve the structure of scenes that contain several objects
|
||||
# Use CADPart, for example, to distinguish between separate objects
|
||||
|
||||
DEFAULT_SUBDIV = 16 # Default subdivision factor for spheres, cones, and cylinders
|
||||
EPSILON = 0.000001
|
||||
|
||||
class Shape:
|
||||
|
||||
# Expects verts in MeshBuilder-ready format, as a n by 3 mdarray
|
||||
# with vertices stored in rows
|
||||
def __init__(self, verts, faces, index_base, name):
|
||||
self.verts = verts
|
||||
self.faces = faces
|
||||
# Those are here for debugging purposes only
|
||||
self.index_base = index_base
|
||||
self.name = name
|
||||
|
||||
class X3DReader(MeshReader):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._supported_extensions = [".x3d"]
|
||||
self._namespaces = {}
|
||||
|
||||
# Main entry point
|
||||
# Reads the file, returns a SceneNode (possibly with nested ones), or None
|
||||
def read(self, file_name):
|
||||
try:
|
||||
self.defs = {}
|
||||
self.shapes = []
|
||||
|
||||
tree = ET.parse(file_name)
|
||||
xml_root = tree.getroot()
|
||||
|
||||
if xml_root.tag != "X3D":
|
||||
return None
|
||||
|
||||
scale = 1000 # Default X3D unit it one meter, while Cura's is one millimeters
|
||||
if xml_root[0].tag == "head":
|
||||
for head_node in xml_root[0]:
|
||||
if head_node.tag == "unit" and head_node.attrib.get("category") == "length":
|
||||
scale *= float(head_node.attrib["conversionFactor"])
|
||||
break
|
||||
xml_scene = xml_root[1]
|
||||
else:
|
||||
xml_scene = xml_root[0]
|
||||
|
||||
if xml_scene.tag != "Scene":
|
||||
return None
|
||||
|
||||
self.transform = Matrix()
|
||||
self.transform.setByScaleFactor(scale)
|
||||
self.index_base = 0
|
||||
|
||||
# Traverse the scene tree, populate the shapes list
|
||||
self.processChildNodes(xml_scene)
|
||||
|
||||
if self.shapes:
|
||||
builder = MeshBuilder()
|
||||
builder.setVertices(numpy.concatenate([shape.verts for shape in self.shapes]))
|
||||
builder.setIndices(numpy.concatenate([shape.faces for shape in self.shapes]))
|
||||
builder.calculateNormals()
|
||||
builder.setFileName(file_name)
|
||||
|
||||
scene = SceneNode()
|
||||
scene.setMeshData(builder.build())
|
||||
scene.setSelectable(True)
|
||||
scene.setName(file_name)
|
||||
scene.getBoundingBox()
|
||||
else:
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
Logger.logException("e", "Exception in X3D reader")
|
||||
return None
|
||||
|
||||
return scene
|
||||
|
||||
# ------------------------- XML tree traversal
|
||||
|
||||
def processNode(self, xml_node):
|
||||
xml_node = self.resolveDefUse(xml_node)
|
||||
if xml_node is None:
|
||||
return
|
||||
|
||||
tag = xml_node.tag
|
||||
if tag in ("Group", "StaticGroup", "CADAssembly", "CADFace", "CADLayer", "Collision"):
|
||||
self.processChildNodes(xml_node)
|
||||
if tag == "CADPart":
|
||||
self.processTransform(xml_node) # TODO: split the parts
|
||||
elif tag == "LOD":
|
||||
self.processNode(xml_node[0])
|
||||
elif tag == "Transform":
|
||||
self.processTransform(xml_node)
|
||||
elif tag == "Shape":
|
||||
self.processShape(xml_node)
|
||||
|
||||
|
||||
def processShape(self, xml_node):
|
||||
# Find the geometry and the appearance inside the Shape
|
||||
geometry = appearance = None
|
||||
for sub_node in xml_node:
|
||||
if sub_node.tag == "Appearance" and not appearance:
|
||||
appearance = self.resolveDefUse(sub_node)
|
||||
elif sub_node.tag in self.geometry_importers and not geometry:
|
||||
geometry = self.resolveDefUse(sub_node)
|
||||
|
||||
# TODO: appearance is completely ignored. At least apply the material color...
|
||||
if not geometry is None:
|
||||
try:
|
||||
self.verts = self.faces = [] # Safeguard
|
||||
self.geometry_importers[geometry.tag](self, geometry)
|
||||
m = self.transform.getData()
|
||||
verts = m.dot(self.verts)[:3].transpose()
|
||||
|
||||
self.shapes.append(Shape(verts, self.faces, self.index_base, geometry.tag))
|
||||
self.index_base += len(verts)
|
||||
|
||||
except Exception:
|
||||
Logger.logException("e", "Exception in X3D reader while reading %s", geometry.tag)
|
||||
|
||||
# Returns the referenced node if the node has USE, the same node otherwise.
|
||||
# May return None is USE points at a nonexistent node
|
||||
# In X3DOM, when both DEF and USE are in the same node, DEF is ignored.
|
||||
# Big caveat: XML element objects may evaluate to boolean False!!!
|
||||
# Don't ever use "if node:", use "if not node is None:" instead
|
||||
def resolveDefUse(self, node):
|
||||
USE = node.attrib.get("USE")
|
||||
if USE:
|
||||
return self.defs.get(USE, None)
|
||||
|
||||
DEF = node.attrib.get("DEF")
|
||||
if DEF:
|
||||
self.defs[DEF] = node
|
||||
return node
|
||||
|
||||
def processChildNodes(self, node):
|
||||
for c in node:
|
||||
self.processNode(c)
|
||||
Job.yieldThread()
|
||||
|
||||
# Since this is a grouping node, will recurse down the tree.
|
||||
# According to the spec, the final transform matrix is:
|
||||
# T * C * R * SR * S * -SR * -C
|
||||
# Where SR corresponds to the rotation matrix to scaleOrientation
|
||||
# C and SR are rather exotic. S, slightly less so.
|
||||
def processTransform(self, node):
|
||||
rot = readRotation(node, "rotation", (0, 0, 1, 0)) # (angle, axisVactor) tuple
|
||||
trans = readVector(node, "translation", (0, 0, 0)) # Vector
|
||||
scale = readVector(node, "scale", (1, 1, 1)) # Vector
|
||||
center = readVector(node, "center", (0, 0, 0)) # Vector
|
||||
scale_orient = readRotation(node, "scaleOrientation", (0, 0, 1, 0)) # (angle, axisVactor) tuple
|
||||
|
||||
# Store the previous transform; in Cura, the default matrix multiplication is in place
|
||||
prev = Matrix(self.transform.getData()) # It's deep copy, I've checked
|
||||
|
||||
# The rest of transform manipulation will be applied in place
|
||||
got_center = (center.x != 0 or center.y != 0 or center.z != 0)
|
||||
|
||||
T = self.transform
|
||||
if trans.x != 0 or trans.y != 0 or trans.z !=0:
|
||||
T.translate(trans)
|
||||
if got_center:
|
||||
T.translate(center)
|
||||
if rot[0] != 0:
|
||||
T.rotateByAxis(*rot)
|
||||
if scale.x != 1 or scale.y != 1 or scale.z != 1:
|
||||
got_scale_orient = scale_orient[0] != 0
|
||||
if got_scale_orient:
|
||||
T.rotateByAxis(*scale_orient)
|
||||
# No scale by vector in place operation in UM
|
||||
S = Matrix()
|
||||
S.setByScaleVector(scale)
|
||||
T.multiply(S)
|
||||
if got_scale_orient:
|
||||
T.rotateByAxis(-scale_orient[0], scale_orient[1])
|
||||
if got_center:
|
||||
T.translate(-center)
|
||||
|
||||
self.processChildNodes(node)
|
||||
self.transform = prev
|
||||
|
||||
# ------------------------- Geometry importers
|
||||
# They are supposed to fill the self.verts and self.faces arrays, the caller will do the rest
|
||||
|
||||
# Primitives
|
||||
|
||||
def processGeometryBox(self, node):
|
||||
(dx, dy, dz) = readFloatArray(node, "size", [2, 2, 2])
|
||||
dx /= 2
|
||||
dy /= 2
|
||||
dz /= 2
|
||||
self.reserveFaceAndVertexCount(12, 8)
|
||||
|
||||
# xz plane at +y, ccw
|
||||
self.addVertex(dx, dy, dz)
|
||||
self.addVertex(-dx, dy, dz)
|
||||
self.addVertex(-dx, dy, -dz)
|
||||
self.addVertex(dx, dy, -dz)
|
||||
# xz plane at -y
|
||||
self.addVertex(dx, -dy, dz)
|
||||
self.addVertex(-dx, -dy, dz)
|
||||
self.addVertex(-dx, -dy, -dz)
|
||||
self.addVertex(dx, -dy, -dz)
|
||||
|
||||
self.addQuad(0, 1, 2, 3) # +y
|
||||
self.addQuad(4, 0, 3, 7) # +x
|
||||
self.addQuad(7, 3, 2, 6) # -z
|
||||
self.addQuad(6, 2, 1, 5) # -x
|
||||
self.addQuad(5, 1, 0, 4) # +z
|
||||
self.addQuad(7, 6, 5, 4) # -y
|
||||
|
||||
# The sphere is subdivided into nr rings and ns segments
|
||||
def processGeometrySphere(self, node):
|
||||
r = readFloat(node, "radius", 0.5)
|
||||
subdiv = readIntArray(node, "subdivision", None)
|
||||
if subdiv:
|
||||
if len(subdiv) == 1:
|
||||
nr = ns = subdiv[0]
|
||||
else:
|
||||
(nr, ns) = subdiv
|
||||
else:
|
||||
nr = ns = DEFAULT_SUBDIV
|
||||
|
||||
lau = pi / nr # Unit angle of latitude (rings) for the given tesselation
|
||||
lou = 2 * pi / ns # Unit angle of longitude (segments)
|
||||
|
||||
self.reserveFaceAndVertexCount(ns*(nr*2 - 2), 2 + (nr - 1)*ns)
|
||||
|
||||
# +y and -y poles
|
||||
self.addVertex(0, r, 0)
|
||||
self.addVertex(0, -r, 0)
|
||||
|
||||
# The non-polar vertices go from x=0, negative z plane counterclockwise -
|
||||
# to -x, to +z, to +x, back to -z
|
||||
for ring in range(1, nr):
|
||||
for seg in range(ns):
|
||||
self.addVertex(-r*sin(lou * seg) * sin(lau * ring),
|
||||
r*cos(lau * ring),
|
||||
-r*cos(lou * seg) * sin(lau * ring))
|
||||
|
||||
vb = 2 + (nr - 2) * ns # First vertex index for the bottom cap
|
||||
|
||||
# Faces go in order: top cap, sides, bottom cap.
|
||||
# Sides go by ring then by segment.
|
||||
|
||||
# Caps
|
||||
# Top cap face vertices go in order: down right up
|
||||
# (starting from +y pole)
|
||||
# Bottom cap goes: up left down (starting from -y pole)
|
||||
for seg in range(ns):
|
||||
self.addTri(0, seg + 2, (seg + 1) % ns + 2)
|
||||
self.addTri(1, vb + (seg + 1) % ns, vb + seg)
|
||||
|
||||
# Sides
|
||||
# Side face vertices go in order: down right upleft, downright up left
|
||||
for ring in range(nr - 2):
|
||||
tvb = 2 + ring * ns
|
||||
# First vertex index for the top edge of the ring
|
||||
bvb = tvb + ns
|
||||
# First vertex index for the bottom edge of the ring
|
||||
for seg in range(ns):
|
||||
nseg = (seg + 1) % ns
|
||||
self.addQuad(tvb + seg, bvb + seg, bvb + nseg, tvb + nseg)
|
||||
|
||||
def processGeometryCone(self, node):
|
||||
r = readFloat(node, "bottomRadius", 1)
|
||||
height = readFloat(node, "height", 2)
|
||||
bottom = readBoolean(node, "bottom", True)
|
||||
side = readBoolean(node, "side", True)
|
||||
n = readInt(node, "subdivision", DEFAULT_SUBDIV)
|
||||
|
||||
d = height / 2
|
||||
angle = 2 * pi / n
|
||||
|
||||
self.reserveFaceAndVertexCount((n if side else 0) + (n-2 if bottom else 0), n+1)
|
||||
|
||||
# Vertex 0 is the apex, vertices 1..n are the bottom
|
||||
self.addVertex(0, d, 0)
|
||||
for i in range(n):
|
||||
self.addVertex(-r * sin(angle * i), -d, -r * cos(angle * i))
|
||||
|
||||
# Side face vertices go: up down right
|
||||
if side:
|
||||
for i in range(n):
|
||||
self.addTri(1 + (i + 1) % n, 0, 1 + i)
|
||||
if bottom:
|
||||
for i in range(2, n):
|
||||
self.addTri(1, i, i+1)
|
||||
|
||||
def processGeometryCylinder(self, node):
|
||||
r = readFloat(node, "radius", 1)
|
||||
height = readFloat(node, "height", 2)
|
||||
bottom = readBoolean(node, "bottom", True)
|
||||
side = readBoolean(node, "side", True)
|
||||
top = readBoolean(node, "top", True)
|
||||
n = readInt(node, "subdivision", DEFAULT_SUBDIV)
|
||||
|
||||
nn = n * 2
|
||||
angle = 2 * pi / n
|
||||
hh = height/2
|
||||
|
||||
self.reserveFaceAndVertexCount((nn if side else 0) + (n - 2 if top else 0) + (n - 2 if bottom else 0), nn)
|
||||
|
||||
# The seam is at x=0, z=-r, vertices go ccw -
|
||||
# to pos x, to neg z, to neg x, back to neg z
|
||||
for i in range(n):
|
||||
rs = -r * sin(angle * i)
|
||||
rc = -r * cos(angle * i)
|
||||
self.addVertex(rs, hh, rc)
|
||||
self.addVertex(rs, -hh, rc)
|
||||
|
||||
if side:
|
||||
for i in range(n):
|
||||
ni = (i + 1) % n
|
||||
self.addQuad(ni * 2 + 1, ni * 2, i * 2, i * 2 + 1)
|
||||
|
||||
for i in range(2, nn-3, 2):
|
||||
if top:
|
||||
self.addTri(0, i, i+2)
|
||||
if bottom:
|
||||
self.addTri(1, i+1, i+3)
|
||||
|
||||
# Semi-primitives
|
||||
|
||||
def processGeometryElevationGrid(self, node):
|
||||
dx = readFloat(node, "xSpacing", 1)
|
||||
dz = readFloat(node, "zSpacing", 1)
|
||||
nx = readInt(node, "xDimension", 0)
|
||||
nz = readInt(node, "zDimension", 0)
|
||||
height = readFloatArray(node, "height", False)
|
||||
ccw = readBoolean(node, "ccw", True)
|
||||
|
||||
if nx <= 0 or nz <= 0 or len(height) < nx*nz:
|
||||
return # That's weird, the wording of the standard suggests grids with zero quads are somehow valid
|
||||
|
||||
self.reserveFaceAndVertexCount(2*(nx-1)*(nz-1), nx*nz)
|
||||
|
||||
for z in range(nz):
|
||||
for x in range(nx):
|
||||
self.addVertex(x * dx, height[z*nx + x], z * dz)
|
||||
|
||||
for z in range(1, nz):
|
||||
for x in range(1, nx):
|
||||
self.addTriFlip((z - 1)*nx + x - 1, z*nx + x, (z - 1)*nx + x, ccw)
|
||||
self.addTriFlip((z - 1)*nx + x - 1, z*nx + x - 1, z*nx + x, ccw)
|
||||
|
||||
def processGeometryExtrusion(self, node):
|
||||
ccw = readBoolean(node, "ccw", True)
|
||||
begin_cap = readBoolean(node, "beginCap", True)
|
||||
end_cap = readBoolean(node, "endCap", True)
|
||||
cross = readFloatArray(node, "crossSection", (1, 1, 1, -1, -1, -1, -1, 1, 1, 1))
|
||||
cross = [(cross[i], cross[i+1]) for i in range(0, len(cross), 2)]
|
||||
spine = readFloatArray(node, "spine", (0, 0, 0, 0, 1, 0))
|
||||
spine = [(spine[i], spine[i+1], spine[i+2]) for i in range(0, len(spine), 3)]
|
||||
orient = readFloatArray(node, "orientation", None)
|
||||
if orient:
|
||||
# This converts X3D's axis/angle rotation to a 3x3 numpy matrix
|
||||
def toRotationMatrix(rot):
|
||||
(x, y, z) = rot[:3]
|
||||
a = rot[3]
|
||||
s = sin(a)
|
||||
c = cos(a)
|
||||
t = 1-c
|
||||
return numpy.array((
|
||||
(x * x * t + c, x * y * t - z*s, x * z * t + y * s),
|
||||
(x * y * t + z*s, y * y * t + c, y * z * t - x * s),
|
||||
(x * z * t - y * s, y * z * t + x * s, z * z * t + c)))
|
||||
|
||||
orient = [toRotationMatrix(orient[i:i+4]) if orient[i+3] != 0 else None for i in range(0, len(orient), 4)]
|
||||
|
||||
scale = readFloatArray(node, "scale", None)
|
||||
if scale:
|
||||
scale = [numpy.array(((scale[i], 0, 0), (0, 1, 0), (0, 0, scale[i+1])))
|
||||
if scale[i] != 1 or scale[i+1] != 1 else None for i in range(0, len(scale), 2)]
|
||||
|
||||
|
||||
# Special treatment for the closed spine and cross section.
|
||||
# Let's save some memory by not creating identical but distinct vertices;
|
||||
# later we'll introduce conditional logic to link the last vertex with
|
||||
# the first one where necessary.
|
||||
crossClosed = cross[0] == cross[-1]
|
||||
if crossClosed:
|
||||
cross = cross[:-1]
|
||||
nc = len(cross)
|
||||
cross = [numpy.array((c[0], 0, c[1])) for c in cross]
|
||||
ncf = nc if crossClosed else nc - 1
|
||||
# Face count along the cross; for closed cross, it's the same as the
|
||||
# respective vertex count
|
||||
|
||||
spine_closed = spine[0] == spine[-1]
|
||||
if spine_closed:
|
||||
spine = spine[:-1]
|
||||
ns = len(spine)
|
||||
spine = [Vector(*s) for s in spine]
|
||||
nsf = ns if spine_closed else ns - 1
|
||||
|
||||
# This will be used for fallback, where the current spine point joins
|
||||
# two collinear spine segments. No need to recheck the case of the
|
||||
# closed spine/last-to-first point juncture; if there's an angle there,
|
||||
# it would kick in on the first iteration of the main loop by spine.
|
||||
def findFirstAngleNormal():
|
||||
for i in range(1, ns - 1):
|
||||
spt = spine[i]
|
||||
z = (spine[i + 1] - spt).cross(spine[i - 1] - spt)
|
||||
if z.length() > EPSILON:
|
||||
return z
|
||||
# All the spines are collinear. Fallback to the rotated source
|
||||
# XZ plane.
|
||||
# TODO: handle the situation where the first two spine points match
|
||||
v = spine[1] - spine[0]
|
||||
orig_y = Vector(0, 1, 0)
|
||||
orig_z = Vector(0, 0, 1)
|
||||
if v.cross(orig_y).length() > EPSILON:
|
||||
# Spine at angle with global y - rotate the z accordingly
|
||||
a = v.cross(orig_y) # Axis of rotation to get to the Z
|
||||
(x, y, z) = a.normalized().getData()
|
||||
s = a.length()/v.length()
|
||||
c = sqrt(1-s*s)
|
||||
t = 1-c
|
||||
m = numpy.array((
|
||||
(x * x * t + c, x * y * t + z*s, x * z * t - y * s),
|
||||
(x * y * t - z*s, y * y * t + c, y * z * t + x * s),
|
||||
(x * z * t + y * s, y * z * t - x * s, z * z * t + c)))
|
||||
orig_z = Vector(*m.dot(orig_z.getData()))
|
||||
return orig_z
|
||||
|
||||
self.reserveFaceAndVertexCount(2*nsf*ncf + (nc - 2 if begin_cap else 0) + (nc - 2 if end_cap else 0), ns*nc)
|
||||
|
||||
z = None
|
||||
for i, spt in enumerate(spine):
|
||||
if (i > 0 and i < ns - 1) or spine_closed:
|
||||
snext = spine[(i + 1) % ns]
|
||||
sprev = spine[(i - 1 + ns) % ns]
|
||||
y = snext - sprev
|
||||
vnext = snext - spt
|
||||
vprev = sprev - spt
|
||||
try_z = vnext.cross(vprev)
|
||||
# Might be zero, then all kinds of fallback
|
||||
if try_z.length() > EPSILON:
|
||||
if z is not None and try_z.dot(z) < 0:
|
||||
try_z = -try_z
|
||||
z = try_z
|
||||
elif not z: # No z, and no previous z.
|
||||
# Look ahead, see if there's at least one point where
|
||||
# spines are not collinear.
|
||||
z = findFirstAngleNormal()
|
||||
elif i == 0: # And non-crossed
|
||||
snext = spine[i + 1]
|
||||
y = snext - spt
|
||||
z = findFirstAngleNormal()
|
||||
else: # last point and not crossed
|
||||
sprev = spine[i - 1]
|
||||
y = spt - sprev
|
||||
# If there's more than one point in the spine, z is already set.
|
||||
# One point in the spline is an error anyway.
|
||||
|
||||
z = z.normalized()
|
||||
y = y.normalized()
|
||||
x = y.cross(z) # Already normalized
|
||||
m = numpy.array(((x.x, y.x, z.x), (x.y, y.y, z.y), (x.z, y.z, z.z)))
|
||||
|
||||
# Columns are the unit vectors for the xz plane for the cross-section
|
||||
if orient:
|
||||
mrot = orient[i] if len(orient) > 1 else orient[0]
|
||||
if not mrot is None:
|
||||
m = m.dot(mrot) # Tested against X3DOM, the result matches, still not sure :(
|
||||
|
||||
if scale:
|
||||
mscale = scale[i] if len(scale) > 1 else scale[0]
|
||||
if not mscale is None:
|
||||
m = m.dot(mscale)
|
||||
|
||||
# First the cross-section 2-vector is scaled,
|
||||
# then rotated (which may make it a 3-vector),
|
||||
# then applied to the xz plane unit vectors
|
||||
|
||||
sptv3 = numpy.array(spt.getData()[:3])
|
||||
for cpt in cross:
|
||||
v = sptv3 + m.dot(cpt)
|
||||
self.addVertex(*v)
|
||||
|
||||
if begin_cap:
|
||||
self.addFace([x for x in range(nc - 1, -1, -1)], ccw)
|
||||
|
||||
# Order of edges in the face: forward along cross, forward along spine,
|
||||
# backward along cross, backward along spine, flipped if now ccw.
|
||||
# This order is assumed later in the texture coordinate assignment;
|
||||
# please don't change without syncing.
|
||||
|
||||
for s in range(ns - 1):
|
||||
for c in range(ncf):
|
||||
self.addQuadFlip(s * nc + c, s * nc + (c + 1) % nc,
|
||||
(s + 1) * nc + (c + 1) % nc, (s + 1) * nc + c, ccw)
|
||||
|
||||
if spine_closed:
|
||||
# The faces between the last and the first spine points
|
||||
b = (ns - 1) * nc
|
||||
for c in range(ncf):
|
||||
self.addQuadFlip(b + c, b + (c + 1) % nc,
|
||||
(c + 1) % nc, c, ccw)
|
||||
|
||||
if end_cap:
|
||||
self.addFace([(ns - 1) * nc + x for x in range(0, nc)], ccw)
|
||||
|
||||
# Triangle meshes
|
||||
|
||||
# Helper for numerous nodes with a Coordinate subnode holding vertices
|
||||
# That all triangle meshes and IndexedFaceSet
|
||||
# num_faces can be a function, in case the face count is a function of vertex count
|
||||
def startCoordMesh(self, node, num_faces):
|
||||
ccw = readBoolean(node, "ccw", True)
|
||||
self.readVertices(node) # This will allocate and fill the vertex array
|
||||
if hasattr(num_faces, "__call__"):
|
||||
num_faces = num_faces(self.getVertexCount())
|
||||
self.reserveFaceCount(num_faces)
|
||||
|
||||
return ccw
|
||||
|
||||
|
||||
def processGeometryIndexedTriangleSet(self, node):
|
||||
index = readIntArray(node, "index", [])
|
||||
num_faces = len(index) // 3
|
||||
ccw = int(self.startCoordMesh(node, num_faces))
|
||||
|
||||
for i in range(0, num_faces*3, 3):
|
||||
self.addTri(index[i + 1 - ccw], index[i + ccw], index[i+2])
|
||||
|
||||
def processGeometryIndexedTriangleStripSet(self, node):
|
||||
strips = readIndex(node, "index")
|
||||
ccw = int(self.startCoordMesh(node, sum([len(strip) - 2 for strip in strips])))
|
||||
|
||||
for strip in strips:
|
||||
sccw = ccw # Running CCW value, reset for each strip
|
||||
for i in range(len(strip) - 2):
|
||||
self.addTri(strip[i + 1 - sccw], strip[i + sccw], strip[i+2])
|
||||
sccw = 1 - sccw
|
||||
|
||||
def processGeometryIndexedTriangleFanSet(self, node):
|
||||
fans = readIndex(node, "index")
|
||||
ccw = int(self.startCoordMesh(node, sum([len(fan) - 2 for fan in fans])))
|
||||
|
||||
for fan in fans:
|
||||
for i in range(1, len(fan) - 1):
|
||||
self.addTri(fan[0], fan[i + 1 - ccw], fan[i + ccw])
|
||||
|
||||
def processGeometryTriangleSet(self, node):
|
||||
ccw = int(self.startCoordMesh(node, lambda num_vert: num_vert // 3))
|
||||
for i in range(0, self.getVertexCount(), 3):
|
||||
self.addTri(i + 1 - ccw, i + ccw, i+2)
|
||||
|
||||
def processGeometryTriangleStripSet(self, node):
|
||||
strips = readIntArray(node, "stripCount", [])
|
||||
ccw = int(self.startCoordMesh(node, sum([n-2 for n in strips])))
|
||||
|
||||
vb = 0
|
||||
for n in strips:
|
||||
sccw = ccw
|
||||
for i in range(n-2):
|
||||
self.addTri(vb + i + 1 - sccw, vb + i + sccw, vb + i + 2)
|
||||
sccw = 1 - sccw
|
||||
vb += n
|
||||
|
||||
def processGeometryTriangleFanSet(self, node):
|
||||
fans = readIntArray(node, "fanCount", [])
|
||||
ccw = int(self.startCoordMesh(node, sum([n-2 for n in fans])))
|
||||
|
||||
vb = 0
|
||||
for n in fans:
|
||||
for i in range(1, n-1):
|
||||
self.addTri(vb, vb + i + 1 - ccw, vb + i + ccw)
|
||||
vb += n
|
||||
|
||||
# Quad geometries from the CAD module, might be relevant for printing
|
||||
|
||||
def processGeometryQuadSet(self, node):
|
||||
ccw = self.startCoordMesh(node, lambda num_vert: 2*(num_vert // 4))
|
||||
for i in range(0, self.getVertexCount(), 4):
|
||||
self.addQuadFlip(i, i+1, i+2, i+3, ccw)
|
||||
|
||||
def processGeometryIndexedQuadSet(self, node):
|
||||
index = readIntArray(node, "index", [])
|
||||
num_quads = len(index) // 4
|
||||
ccw = self.startCoordMesh(node, num_quads*2)
|
||||
|
||||
for i in range(0, num_quads*4, 4):
|
||||
self.addQuadFlip(index[i], index[i+1], index[i+2], index[i+3], ccw)
|
||||
|
||||
# 2D polygon geometries
|
||||
# Won't work for now, since Cura expects every mesh to have a nontrivial convex hull
|
||||
# The only way around that is merging meshes.
|
||||
|
||||
def processGeometryDisk2D(self, node):
|
||||
innerRadius = readFloat(node, "innerRadius", 0)
|
||||
outerRadius = readFloat(node, "outerRadius", 1)
|
||||
n = readInt(node, "subdivision", DEFAULT_SUBDIV)
|
||||
|
||||
angle = 2 * pi / n
|
||||
|
||||
self.reserveFaceAndVertexCount(n*4 if innerRadius else n-2, n*2 if innerRadius else n)
|
||||
|
||||
for i in range(n):
|
||||
s = sin(angle * i)
|
||||
c = cos(angle * i)
|
||||
self.addVertex(outerRadius*c, outerRadius*s, 0)
|
||||
if innerRadius:
|
||||
self.addVertex(innerRadius*c, innerRadius*s, 0)
|
||||
ni = (i+1) % n
|
||||
self.addQuad(2*i, 2*ni, 2*ni+1, 2*i+1)
|
||||
|
||||
if not innerRadius:
|
||||
for i in range(2, n):
|
||||
self.addTri(0, i-1, i)
|
||||
|
||||
def processGeometryRectangle2D(self, node):
|
||||
(x, y) = readFloatArray(node, "size", (2, 2))
|
||||
self.reserveFaceAndVertexCount(2, 4)
|
||||
self.addVertex(-x/2, -y/2, 0)
|
||||
self.addVertex(x/2, -y/2, 0)
|
||||
self.addVertex(x/2, y/2, 0)
|
||||
self.addVertex(-x/2, y/2, 0)
|
||||
self.addQuad(0, 1, 2, 3)
|
||||
|
||||
def processGeometryTriangleSet2D(self, node):
|
||||
verts = readFloatArray(node, "vertices", ())
|
||||
num_faces = len(verts) // 6;
|
||||
verts = [(verts[i], verts[i+1], 0) for i in range(0, 6 * num_faces, 2)]
|
||||
self.reserveFaceAndVertexCount(num_faces, num_faces * 3)
|
||||
for vert in verts:
|
||||
self.addVertex(*vert)
|
||||
|
||||
# The front face is on the +Z side, so CCW is a variable
|
||||
for i in range(0, num_faces*3, 3):
|
||||
a = Vector(*verts[i+2]) - Vector(*verts[i])
|
||||
b = Vector(*verts[i+1]) - Vector(*verts[i])
|
||||
self.addTriFlip(i, i+1, i+2, a.x*b.y > a.y*b.x)
|
||||
|
||||
# General purpose polygon mesh
|
||||
|
||||
def processGeometryIndexedFaceSet(self, node):
|
||||
faces = readIndex(node, "coordIndex")
|
||||
ccw = self.startCoordMesh(node, sum([len(face) - 2 for face in faces]))
|
||||
|
||||
for face in faces:
|
||||
if len(face) == 3:
|
||||
self.addTriFlip(face[0], face[1], face[2], ccw)
|
||||
elif len(face) > 3:
|
||||
self.addFace(face, ccw)
|
||||
|
||||
geometry_importers = {
|
||||
"IndexedFaceSet": processGeometryIndexedFaceSet,
|
||||
"IndexedTriangleSet": processGeometryIndexedTriangleSet,
|
||||
"IndexedTriangleStripSet": processGeometryIndexedTriangleStripSet,
|
||||
"IndexedTriangleFanSet": processGeometryIndexedTriangleFanSet,
|
||||
"TriangleSet": processGeometryTriangleSet,
|
||||
"TriangleStripSet": processGeometryTriangleStripSet,
|
||||
"TriangleFanSet": processGeometryTriangleFanSet,
|
||||
"QuadSet": processGeometryQuadSet,
|
||||
"IndexedQuadSet": processGeometryIndexedQuadSet,
|
||||
"TriangleSet2D": processGeometryTriangleSet2D,
|
||||
"Rectangle2D": processGeometryRectangle2D,
|
||||
"Disk2D": processGeometryDisk2D,
|
||||
"ElevationGrid": processGeometryElevationGrid,
|
||||
"Extrusion": processGeometryExtrusion,
|
||||
"Sphere": processGeometrySphere,
|
||||
"Box": processGeometryBox,
|
||||
"Cylinder": processGeometryCylinder,
|
||||
"Cone": processGeometryCone
|
||||
}
|
||||
|
||||
# Parses the Coordinate.@point field, fills the verts array.
|
||||
def readVertices(self, node):
|
||||
for c in node:
|
||||
if c.tag == "Coordinate":
|
||||
c = self.resolveDefUse(c)
|
||||
if not c is None:
|
||||
pt = c.attrib.get("point")
|
||||
if pt:
|
||||
co = [float(x) for x in pt.split()]
|
||||
num_verts = len(co) // 3
|
||||
self.verts = numpy.empty((4, num_verts), dtype=numpy.float32)
|
||||
self.verts[3,:] = numpy.ones((num_verts), dtype=numpy.float32)
|
||||
# Group by three
|
||||
for i in range(num_verts):
|
||||
self.verts[:3,i] = co[3*i:3*i+3]
|
||||
|
||||
# Mesh builder helpers
|
||||
|
||||
def reserveFaceAndVertexCount(self, num_faces, num_verts):
|
||||
# Unlike the Cura MeshBuilder, we use 4-vectors stored as columns for easier transform
|
||||
self.verts = numpy.zeros((4, num_verts), dtype=numpy.float32)
|
||||
self.verts[3,:] = numpy.ones((num_verts), dtype=numpy.float32)
|
||||
self.num_verts = 0
|
||||
self.reserveFaceCount(num_faces)
|
||||
|
||||
def reserveFaceCount(self, num_faces):
|
||||
self.faces = numpy.zeros((num_faces, 3), dtype=numpy.int32)
|
||||
self.num_faces = 0
|
||||
|
||||
def getVertexCount(self):
|
||||
return self.verts.shape[1]
|
||||
|
||||
def addVertex(self, x, y, z):
|
||||
self.verts[0, self.num_verts] = x
|
||||
self.verts[1, self.num_verts] = y
|
||||
self.verts[2, self.num_verts] = z
|
||||
self.num_verts += 1
|
||||
|
||||
# Indices are 0-based for this shape, but they won't be zero-based in the merged mesh
|
||||
def addTri(self, a, b, c):
|
||||
self.faces[self.num_faces, 0] = self.index_base + a
|
||||
self.faces[self.num_faces, 1] = self.index_base + b
|
||||
self.faces[self.num_faces, 2] = self.index_base + c
|
||||
self.num_faces += 1
|
||||
|
||||
def addTriFlip(self, a, b, c, ccw):
|
||||
if ccw:
|
||||
self.addTri(a, b, c)
|
||||
else:
|
||||
self.addTri(b, a, c)
|
||||
|
||||
# Needs to be convex, but not necessaily planar
|
||||
# Assumed ccw, cut along the ac diagonal
|
||||
def addQuad(self, a, b, c, d):
|
||||
self.addTri(a, b, c)
|
||||
self.addTri(c, d, a)
|
||||
|
||||
def addQuadFlip(self, a, b, c, d, ccw):
|
||||
if ccw:
|
||||
self.addTri(a, b, c)
|
||||
self.addTri(c, d, a)
|
||||
else:
|
||||
self.addTri(a, c, b)
|
||||
self.addTri(c, a, d)
|
||||
|
||||
|
||||
# Arbitrary polygon triangulation.
|
||||
# Doesn't assume convexity and doesn't check the "convex" flag in the file.
|
||||
# Works by the "cutting of ears" algorithm:
|
||||
# - Find an outer vertex with the smallest angle and no vertices inside its adjacent triangle
|
||||
# - Remove the triangle at that vertex
|
||||
# - Repeat until done
|
||||
# Vertex coordinates are supposed to be already set
|
||||
def addFace(self, indices, ccw):
|
||||
# Resolve indices to coordinates for faster math
|
||||
face = [Vector(data=self.verts[0:3, i]) for i in indices]
|
||||
|
||||
# Need a normal to the plane so that we can know which vertices form inner angles
|
||||
normal = findOuterNormal(face)
|
||||
|
||||
if not normal: # Couldn't find an outer edge, non-planar polygon maybe?
|
||||
return
|
||||
|
||||
# Find the vertex with the smallest inner angle and no points inside, cut off. Repeat until done
|
||||
n = len(face)
|
||||
vi = [i for i in range(n)] # We'll be using this to kick vertices from the face
|
||||
while n > 3:
|
||||
max_cos = EPSILON # We don't want to check anything on Pi angles
|
||||
i_min = 0 # max cos corresponds to min angle
|
||||
for i in range(n):
|
||||
inext = (i + 1) % n
|
||||
iprev = (i + n - 1) % n
|
||||
v = face[vi[i]]
|
||||
next = face[vi[inext]] - v
|
||||
prev = face[vi[iprev]] - v
|
||||
nextXprev = next.cross(prev)
|
||||
if nextXprev.dot(normal) > EPSILON: # If it's an inner angle
|
||||
cos = next.dot(prev) / (next.length() * prev.length())
|
||||
if cos > max_cos:
|
||||
# Check if there are vertices inside the triangle
|
||||
no_points_inside = True
|
||||
for j in range(n):
|
||||
if j != i and j != iprev and j != inext:
|
||||
vx = face[vi[j]] - v
|
||||
if pointInsideTriangle(vx, next, prev, nextXprev):
|
||||
no_points_inside = False
|
||||
break
|
||||
|
||||
if no_points_inside:
|
||||
max_cos = cos
|
||||
i_min = i
|
||||
|
||||
self.addTriFlip(indices[vi[(i_min + n - 1) % n]], indices[vi[i_min]], indices[vi[(i_min + 1) % n]], ccw)
|
||||
vi.pop(i_min)
|
||||
n -= 1
|
||||
self.addTriFlip(indices[vi[0]], indices[vi[1]], indices[vi[2]], ccw)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# X3D field parsers
|
||||
# ------------------------------------------------------------
|
||||
def readFloatArray(node, attr, default):
|
||||
s = node.attrib.get(attr)
|
||||
if not s:
|
||||
return default
|
||||
return [float(x) for x in s.split()]
|
||||
|
||||
def readIntArray(node, attr, default):
|
||||
s = node.attrib.get(attr)
|
||||
if not s:
|
||||
return default
|
||||
return [int(x, 0) for x in s.split()]
|
||||
|
||||
def readFloat(node, attr, default):
|
||||
s = node.attrib.get(attr)
|
||||
if not s:
|
||||
return default
|
||||
return float(s)
|
||||
|
||||
def readInt(node, attr, default):
|
||||
s = node.attrib.get(attr)
|
||||
if not s:
|
||||
return default
|
||||
return int(s, 0)
|
||||
|
||||
def readBoolean(node, attr, default):
|
||||
s = node.attrib.get(attr)
|
||||
if not s:
|
||||
return default
|
||||
return s.lower() == "true"
|
||||
|
||||
def readVector(node, attr, default):
|
||||
v = readFloatArray(node, attr, default)
|
||||
return Vector(v[0], v[1], v[2])
|
||||
|
||||
def readRotation(node, attr, default):
|
||||
v = readFloatArray(node, attr, default)
|
||||
return (v[3], Vector(v[0], v[1], v[2]))
|
||||
|
||||
# Returns the -1-separated runs
|
||||
def readIndex(node, attr):
|
||||
v = readIntArray(node, attr, [])
|
||||
chunks = []
|
||||
chunk = []
|
||||
for i in range(len(v)):
|
||||
if v[i] == -1:
|
||||
if chunk:
|
||||
chunks.append(chunk)
|
||||
chunk = []
|
||||
else:
|
||||
chunk.append(v[i])
|
||||
if chunk:
|
||||
chunks.append(chunk)
|
||||
return chunks
|
||||
|
||||
# Given a face as a sequence of vectors, returns a normal to the polygon place that forms a right triple
|
||||
# with a vector along the polygon sequence and a vector backwards
|
||||
def findOuterNormal(face):
|
||||
n = len(face)
|
||||
for i in range(n):
|
||||
for j in range(i+1, n):
|
||||
edge = face[j] - face[i]
|
||||
if edge.length() > EPSILON:
|
||||
edge = edge.normalized()
|
||||
prev_rejection = Vector()
|
||||
is_outer = True
|
||||
for k in range(n):
|
||||
if k != i and k != j:
|
||||
pt = face[k] - face[i]
|
||||
pte = pt.dot(edge)
|
||||
rejection = pt - edge*pte
|
||||
if rejection.dot(prev_rejection) < -EPSILON: # points on both sides of the edge - not an outer one
|
||||
is_outer = False
|
||||
break
|
||||
elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability
|
||||
prev_rejection = rejection
|
||||
|
||||
if is_outer: # Found an outer edge, prev_rejection is the rejection inside the face. Generate a normal.
|
||||
return edge.cross(prev_rejection)
|
||||
|
||||
return False
|
||||
|
||||
# Given two *collinear* vectors a and b, returns the coefficient that takes b to a.
|
||||
# No error handling.
|
||||
# For stability, taking the ration between the biggest coordinates would be better...
|
||||
def ratio(a, b):
|
||||
if b.x > EPSILON or b.x < -EPSILON:
|
||||
return a.x / b.x
|
||||
elif b.y > EPSILON or b.y < -EPSILON:
|
||||
return a.y / b.y
|
||||
else:
|
||||
return a.z / b.z
|
||||
|
||||
def pointInsideTriangle(vx, next, prev, nextXprev):
|
||||
vxXprev = vx.cross(prev)
|
||||
r = ratio(vxXprev, nextXprev)
|
||||
if r < 0:
|
||||
return False
|
||||
vxXnext = vx.cross(next);
|
||||
s = -ratio(vxXnext, nextXprev)
|
||||
return s > 0 and (s + r) < 1
|
||||
|
26
plugins/X3DReader/__init__.py
Normal file
26
plugins/X3DReader/__init__.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Seva Alekseyev with National Institutes of Health, 2016
|
||||
|
||||
from . import X3DReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "X3D Reader"),
|
||||
"author": "Seva Alekseyev",
|
||||
"version": "0.5",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides support for reading X3D files."),
|
||||
"api": 3
|
||||
},
|
||||
"mesh_reader": [
|
||||
{
|
||||
"extension": "x3d",
|
||||
"description": catalog.i18nc("@item:inlistbox", "X3D File")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def register(app):
|
||||
return { "mesh_reader": X3DReader.X3DReader() }
|
|
@ -636,6 +636,7 @@
|
|||
"default_value": 0.4,
|
||||
"type": "float",
|
||||
"value": "line_width",
|
||||
"enabled": "infill_sparse_density > 0",
|
||||
"settable_per_mesh": true
|
||||
},
|
||||
"skirt_brim_line_width":
|
||||
|
@ -947,6 +948,7 @@
|
|||
"zigzag": "Zig Zag"
|
||||
},
|
||||
"default_value": "grid",
|
||||
"enabled": "infill_sparse_density > 0",
|
||||
"value": "'lines' if infill_sparse_density > 25 else 'grid'",
|
||||
"settable_per_mesh": true
|
||||
},
|
||||
|
@ -960,7 +962,7 @@
|
|||
"value": "10 if infill_sparse_density < 95 and infill_pattern != 'concentric' else 0",
|
||||
"minimum_value_warning": "-50",
|
||||
"maximum_value_warning": "100",
|
||||
"enabled": "infill_pattern != 'concentric'",
|
||||
"enabled": "infill_sparse_density > 0 and infill_pattern != 'concentric'",
|
||||
"settable_per_mesh": true,
|
||||
"children":
|
||||
{
|
||||
|
@ -974,7 +976,7 @@
|
|||
"minimum_value_warning": "-0.5 * machine_nozzle_size",
|
||||
"maximum_value_warning": "machine_nozzle_size",
|
||||
"value": "infill_line_width * infill_overlap / 100 if infill_sparse_density < 95 and infill_pattern != 'concentric' else 0",
|
||||
"enabled": "infill_pattern != 'concentric'",
|
||||
"enabled": "infill_sparse_density > 0 and infill_pattern != 'concentric'",
|
||||
"settable_per_mesh": true
|
||||
}
|
||||
}
|
||||
|
@ -1015,6 +1017,7 @@
|
|||
"value": "wall_line_width_0 / 4 if wall_line_count == 1 else wall_line_width_x / 4",
|
||||
"minimum_value_warning": "0",
|
||||
"maximum_value_warning": "machine_nozzle_size",
|
||||
"enabled": "infill_sparse_density > 0",
|
||||
"settable_per_mesh": true
|
||||
},
|
||||
"infill_sparse_thickness":
|
||||
|
@ -1028,6 +1031,7 @@
|
|||
"maximum_value_warning": "0.32",
|
||||
"maximum_value": "layer_height * 8",
|
||||
"value": "layer_height",
|
||||
"enabled": "infill_sparse_density > 0",
|
||||
"settable_per_mesh": true
|
||||
},
|
||||
"gradual_infill_steps":
|
||||
|
@ -1039,6 +1043,7 @@
|
|||
"minimum_value": "0",
|
||||
"maximum_value_warning": "4",
|
||||
"maximum_value": "20 - math.log(infill_line_distance) / math.log(2)",
|
||||
"enabled": "infill_sparse_density > 0",
|
||||
"settable_per_mesh": true
|
||||
},
|
||||
"gradual_infill_step_height":
|
||||
|
@ -1050,7 +1055,7 @@
|
|||
"default_value": 5.0,
|
||||
"minimum_value": "0.0001",
|
||||
"maximum_value_warning": "100",
|
||||
"enabled": "gradual_infill_steps > 0",
|
||||
"enabled": "infill_sparse_density > 0 and gradual_infill_steps > 0",
|
||||
"settable_per_mesh": true
|
||||
},
|
||||
"infill_before_walls":
|
||||
|
@ -1059,6 +1064,7 @@
|
|||
"description": "Print the infill before printing the walls. Printing the walls first may lead to more accurate walls, but overhangs print worse. Printing the infill first leads to sturdier walls, but the infill pattern might sometimes show through the surface.",
|
||||
"type": "bool",
|
||||
"default_value": true,
|
||||
"enabled": "infill_sparse_density > 0",
|
||||
"settable_per_mesh": true
|
||||
}
|
||||
}
|
||||
|
@ -1411,6 +1417,7 @@
|
|||
"maximum_value_warning": "150",
|
||||
"default_value": 60,
|
||||
"value": "speed_print",
|
||||
"enabled": "infill_sparse_density > 0",
|
||||
"settable_per_mesh": true
|
||||
},
|
||||
"speed_wall":
|
||||
|
@ -1685,7 +1692,7 @@
|
|||
"maximum_value_warning": "10000",
|
||||
"default_value": 3000,
|
||||
"value": "acceleration_print",
|
||||
"enabled": "acceleration_enabled",
|
||||
"enabled": "acceleration_enabled and infill_sparse_density > 0",
|
||||
"settable_per_mesh": true
|
||||
},
|
||||
"acceleration_wall": {
|
||||
|
@ -1906,7 +1913,7 @@
|
|||
"maximum_value_warning": "50",
|
||||
"default_value": 20,
|
||||
"value": "jerk_print",
|
||||
"enabled": "jerk_enabled",
|
||||
"enabled": "jerk_enabled and infill_sparse_density > 0",
|
||||
"settable_per_mesh": true
|
||||
},
|
||||
"jerk_wall": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue