Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ android {
applicationId 'org.crabcore.u1control'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 17
versionName "1.2.7"
versionCode 18
versionName "1.2.8"

buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1342,7 +1342,7 @@ class HelixModelPreviewActivity : Activity() {
},
initialTool = initialTool,
sliceSettings = sliceSettings,
materialProfiles = HelixSliceRunner.parseMaterialProfiles(materialProfilesJson),
materialProfilesJson = materialProfilesJson,
)
val result = outcome.result
runOnUiThread {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ object HelixSliceRunner {
onProgress: (Int, String) -> Unit,
initialTool: Int = 0,
sliceSettings: HelixSliceSettings = HelixSliceSettings(),
materialProfiles: List<MaterialProfile> = emptyList(),
materialProfilesJson: String? = null,
forceExtruderCount: Int? = null,
configure: SliceConfig.() -> Unit = {},
): Outcome {
if (!slicing.compareAndSet(false, true)) throw BusyError()
Expand Down Expand Up @@ -100,20 +101,24 @@ object HelixSliceRunner {

// Multicolor models carry more than one filament in their project config.
// For those we keep the engine's multi-tool output; only single-colour
// prints get pinned to the user's chosen loaded head.
// prints get pinned to the user's chosen loaded head. forceExtruderCount
// (per-colour remap) lifts the tool count to cover every mapped slot even
// when the file declares fewer filaments than the user routed to.
val filamentCount = readFilamentCount(lib)
val multicolor = filamentCount > 1
val effectiveCount = maxOf(filamentCount, forceExtruderCount ?: 1).coerceIn(1, 4)
val multicolor = effectiveCount > 1

val (startGcode, endGcode) = readMachineGcode(context)
val selectedTool = initialTool.coerceIn(0, 3)
val materialProfiles = parseMaterialProfiles(materialProfilesJson)
val config = SliceConfig(
machineStartGcode = startGcode,
machineEndGcode = endGcode,
).apply {
sliceSettings.applyTo(this)
configure()
applyMaterialProfiles(materialProfiles, selectedTool)
if (multicolor) configureMultiTool(filamentCount) else forceSingleTool(selectedTool)
if (multicolor) configureMultiTool(effectiveCount) else forceSingleTool(selectedTool)
// Wipe tower position shown/dragged on the prepare screen.
PrepareSession.towerPositionFor(path)?.let { (tx, ty) ->
wipeTowerX = tx
Expand Down Expand Up @@ -160,7 +165,11 @@ object HelixSliceRunner {
false
}
if (result != null && result.success) {
LastSliceStore.record(path, result, selectedTool, toolResult.usedToolMask)
LastSliceStore.record(
path, result, selectedTool, toolResult.usedToolMask,
sliceSettingsJson = sliceSettings.toJson(),
materialProfilesJson = materialProfilesJson,
)
}
return Outcome(result, thumbnailsInjected, selectedTool, toolResult.usedToolMask)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import android.widget.TextView
import android.widget.ArrayAdapter
import com.u1.slicer.data.SliceConfig
import org.crabcore.u1control.R
import org.json.JSONObject

/** User-facing slice overrides on the prepare screen (Phase 1 — SliceConfig fields only). */
data class HelixSliceSettings(
Expand Down Expand Up @@ -49,6 +50,26 @@ data class HelixSliceSettings(

fun hasInfillOverride(): Boolean = infillDensity >= 0f || infillPattern != "default"

/** Round-trip serialization so a background re-slice can replay the exact
* prepare-screen overrides (supports/brim/infill/ironing). */
fun toJson(): String = JSONObject().apply {
put("supportsEnabled", supportsEnabled)
put("supportType", supportType)
put("supportAngle", supportAngle)
put("supportFilament", supportFilament)
put("supportInterfaceFilament", supportInterfaceFilament)
put("supportBuildPlateOnly", supportBuildPlateOnly)
put("supportPattern", supportPattern)
put("brimWidthMm", brimWidthMm.toDouble())
put("infillDensity", infillDensity.toDouble())
put("infillPattern", infillPattern)
put("ironingType", ironingType)
put("ironingPattern", ironingPattern)
put("ironingFlow", ironingFlow)
put("ironingSpacing", ironingSpacing.toDouble())
put("ironingSpeed", ironingSpeed)
}.toString()

fun applyTo(config: SliceConfig) {
config.supportEnabled = supportsEnabled
if (supportsEnabled) {
Expand Down Expand Up @@ -127,6 +148,31 @@ data class HelixSliceSettings(
brimWidth?.let { settings.brimWidthMm = it.toFloat() }
return settings
}

/** Reconstruct from [toJson] output; null/blank/garbage → defaults. */
fun fromJson(json: String?): HelixSliceSettings {
if (json.isNullOrBlank()) return HelixSliceSettings()
return runCatching {
val o = JSONObject(json)
HelixSliceSettings(
supportsEnabled = o.optBoolean("supportsEnabled"),
supportType = o.optString("supportType", "normal(auto)"),
supportAngle = o.optInt("supportAngle", 30).coerceIn(0, 90),
supportFilament = o.optInt("supportFilament", 0),
supportInterfaceFilament = o.optInt("supportInterfaceFilament", -1),
supportBuildPlateOnly = o.optBoolean("supportBuildPlateOnly"),
supportPattern = o.optString("supportPattern", "default"),
brimWidthMm = o.optDouble("brimWidthMm", 0.0).toFloat(),
infillDensity = o.optDouble("infillDensity", -1.0).toFloat(),
infillPattern = o.optString("infillPattern", "default"),
ironingType = o.optString("ironingType", "no ironing"),
ironingPattern = o.optString("ironingPattern", "zig-zag"),
ironingFlow = o.optInt("ironingFlow", 10),
ironingSpacing = o.optDouble("ironingSpacing", 0.15).toFloat(),
ironingSpeed = o.optInt("ironingSpeed", 30),
)
}.getOrDefault(HelixSliceSettings())
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,71 @@ class HelixSlicerModule(
}
}

/**
* Repacks [path] (any 3MF the engine accepts — typically an extracted plate)
* into a temp 3MF where every object is reassigned to [targetTool] (0-based)
* and the multi-filament project config is stripped, so a re-slice produces
* single-tool gcode in that one material. Resolves the new filePath.
*/
@ReactMethod
fun collapseModel(path: String, targetTool: Int, promise: Promise) {
try {
val file = File(path.removePrefix("file://"))
if (!file.exists()) {
promise.reject("E_NO_FILE", "Model file not found: ${file.absolutePath}")
return
}
val outFile = File(reactApplicationContext.filesDir, "collapsed_tool_${targetTool}.3mf")
PlateExtractor.collapseToSingleExtruder(file, targetTool, outFile)
if (!outFile.exists() || outFile.length() == 0L) {
promise.reject("E_COLLAPSE", "Failed to collapse model to tool $targetTool.")
return
}
promise.resolve(outFile.absolutePath)
} catch (error: Throwable) {
promise.reject("E_COLLAPSE", error.message, error)
}
}

/**
* Repacks [path] so each object's extruder is remapped per [extruderMapJson]
* (JSON object { "<1-based source extruder>": <0-based target slot>, ... }),
* keeping the file multi-filament so the re-slice stays multi-colour. Used for
* per-colour remap (e.g. red->slot 2, blue->slot 3). Resolves the new filePath.
*/
@ReactMethod
fun remapModel(path: String, extruderMapJson: String, promise: Promise) {
try {
val file = File(path.removePrefix("file://"))
if (!file.exists()) {
promise.reject("E_NO_FILE", "Model file not found: ${file.absolutePath}")
return
}
val map = HashMap<Int, Int>()
val obj = JSONObject(extruderMapJson)
val keys = obj.keys()
while (keys.hasNext()) {
val key = keys.next()
key.toIntOrNull()?.let { srcExtruder ->
map[srcExtruder] = obj.getInt(key).coerceIn(0, 3)
}
}
if (map.isEmpty()) {
promise.reject("E_REMAP", "Extruder map is empty.")
return
}
val outFile = File(reactApplicationContext.filesDir, "remapped_${System.nanoTime()}.3mf")
PlateExtractor.remapExtruders(file, map, outFile)
if (!outFile.exists() || outFile.length() == 0L) {
promise.reject("E_REMAP", "Failed to remap model.")
return
}
promise.resolve(outFile.absolutePath)
} catch (error: Throwable) {
promise.reject("E_REMAP", error.message, error)
}
}

private fun emitExtractProgress(percent: Int, phase: String) {
val params = Arguments.createMap().apply {
putInt("percent", percent)
Expand Down Expand Up @@ -591,6 +656,8 @@ class HelixSlicerModule(
putDouble("estimatedFilamentGrams", LastSliceStore.estimatedFilamentGrams.toDouble())
putInt("initialTool", LastSliceStore.initialTool)
putInt("usedToolMask", LastSliceStore.usedToolMask)
LastSliceStore.sliceSettingsJson?.let { putString("sliceSettings", it) }
LastSliceStore.materialProfilesJson?.let { putString("materialProfiles", it) }
},
)
}
Expand Down Expand Up @@ -709,25 +776,37 @@ class HelixSlicerModule(
?.coerceIn(0, 3)
?: 0
val o = options
val sliceSettings = HelixSliceSettings.fromBridgeOptions(
supportEnabled = o?.takeIf { it.hasKey("supportEnabled") }?.getBoolean("supportEnabled"),
supportType = o?.takeIf { it.hasKey("supportType") }?.getString("supportType"),
supportAngle = o?.takeIf { it.hasKey("supportAngle") }?.getDouble("supportAngle"),
supportFilament = o?.takeIf { it.hasKey("supportFilament") }?.getInt("supportFilament"),
supportInterfaceFilament = o?.takeIf { it.hasKey("supportInterfaceFilament") }
?.getInt("supportInterfaceFilament"),
supportBuildPlateOnly = o?.takeIf { it.hasKey("supportBuildPlateOnly") }
?.getBoolean("supportBuildPlateOnly"),
supportPattern = o?.takeIf { it.hasKey("supportPattern") }?.getString("supportPattern"),
brimWidth = o?.takeIf { it.hasKey("brimWidth") }?.getDouble("brimWidth"),
)
// Re-slice path: RN may pass the full sliceSettings JSON captured from
// the prepare screen (covers supports/brim/infill/ironing), which takes
// precedence over the granular bridge keys. Individual keys still layer
// on top for callers that use them.
val sliceSettings = if (o?.hasKey("sliceSettings") == true) {
HelixSliceSettings.fromJson(o.getString("sliceSettings"))
} else {
HelixSliceSettings.fromBridgeOptions(
supportEnabled = o?.takeIf { it.hasKey("supportEnabled") }?.getBoolean("supportEnabled"),
supportType = o?.takeIf { it.hasKey("supportType") }?.getString("supportType"),
supportAngle = o?.takeIf { it.hasKey("supportAngle") }?.getDouble("supportAngle"),
supportFilament = o?.takeIf { it.hasKey("supportFilament") }?.getInt("supportFilament"),
supportInterfaceFilament = o?.takeIf { it.hasKey("supportInterfaceFilament") }
?.getInt("supportInterfaceFilament"),
supportBuildPlateOnly = o?.takeIf { it.hasKey("supportBuildPlateOnly") }
?.getBoolean("supportBuildPlateOnly"),
supportPattern = o?.takeIf { it.hasKey("supportPattern") }?.getString("supportPattern"),
brimWidth = o?.takeIf { it.hasKey("brimWidth") }?.getDouble("brimWidth"),
)
}
val materialProfilesJson = o?.takeIf { it.hasKey("materialProfiles") }?.getString("materialProfiles")
val forceExtruderCount = o?.takeIf { it.hasKey("forceExtruderCount") }?.getInt("forceExtruderCount")
val outcome = HelixSliceRunner.run(
reactApplicationContext,
lib,
path,
onProgress = { pct, stage -> emitProgress(pct, stage) },
initialTool = initialTool,
sliceSettings = sliceSettings,
materialProfilesJson = materialProfilesJson,
forceExtruderCount = forceExtruderCount,
) {
o?.let { opts ->
if (opts.hasKey("layerHeight")) layerHeight = opts.getDouble("layerHeight").toFloat()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ package org.crabcore.u1control.slicing
import com.u1.slicer.data.SliceResult

/** Last successful slice — lets the RN Slice Lab tab offer upload/print after
* slicing from the native prepare screen (which never calls back into JS). */
* slicing from the native prepare screen (which never calls back into JS).
*
* Also keeps the sliceSettings + materialProfiles JSON so a background
* re-slice (e.g. "print this in a different loaded filament") can replay the
* exact prepare-screen overrides and material temperatures. */
object LastSliceStore {
@Volatile var modelPath: String? = null
@Volatile var gcodePath: String? = null
Expand All @@ -12,8 +16,17 @@ object LastSliceStore {
@Volatile var estimatedFilamentGrams: Float = 0f
@Volatile var initialTool: Int = 0
@Volatile var usedToolMask: Int = 1
@Volatile var sliceSettingsJson: String? = null
@Volatile var materialProfilesJson: String? = null

fun record(model: String, result: SliceResult, initialTool: Int = 0, usedToolMask: Int = 1) {
fun record(
model: String,
result: SliceResult,
initialTool: Int = 0,
usedToolMask: Int = 1,
sliceSettingsJson: String? = null,
materialProfilesJson: String? = null,
) {
if (!result.success || result.gcodePath.isBlank()) return
modelPath = model
gcodePath = result.gcodePath
Expand All @@ -22,6 +35,8 @@ object LastSliceStore {
estimatedFilamentGrams = result.estimatedFilamentGrams
this.initialTool = initialTool
this.usedToolMask = usedToolMask
this.sliceSettingsJson = sliceSettingsJson
this.materialProfilesJson = materialProfilesJson
}

fun clear() {
Expand All @@ -32,5 +47,7 @@ object LastSliceStore {
estimatedFilamentGrams = 0f
initialTool = 0
usedToolMask = 1
sliceSettingsJson = null
materialProfilesJson = null
}
}
Loading