lithop 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/Cargo.lock +1 -1
- data/README.md +14 -11
- data/RELEASING.md +3 -3
- data/docs/README.md +4 -3
- data/docs/artifacts.md +27 -15
- data/docs/bambu-studio.md +12 -10
- data/docs/calibration.md +4 -4
- data/docs/color-model.md +20 -18
- data/docs/command.md +28 -12
- data/docs/config.md +42 -30
- data/docs/getting-started.md +12 -6
- data/docs/local-ui.md +12 -9
- data/docs/tiling.md +18 -19
- data/docs/troubleshooting.md +3 -3
- data/ext/lithop_native/Cargo.toml +1 -1
- data/lib/lithop/calibration/target.rb +5 -5
- data/lib/lithop/cli/image_adjustment_options.rb +33 -0
- data/lib/lithop/cli/image_options.rb +5 -18
- data/lib/lithop/cli/options.rb +4 -4
- data/lib/lithop/export/color_metadata.rb +46 -23
- data/lib/lithop/export/generation.rb +8 -10
- data/lib/lithop/export/planning.rb +82 -45
- data/lib/lithop/export/print_readme.rb +20 -4
- data/lib/lithop/formats/three_mf/bambu_metadata.rb +11 -8
- data/lib/lithop/formats/three_mf/model_writer.rb +2 -5
- data/lib/lithop/image/pipeline.rb +40 -26
- data/lib/lithop/image/svg_loader.rb +11 -2
- data/lib/lithop/layer_structure.rb +0 -9
- data/lib/lithop/model/filament_profile.rb +3 -3
- data/lib/lithop/model/material_display_map.rb +2 -1
- data/lib/lithop/model/z_region_model.rb +44 -0
- data/lib/lithop/settings.rb +147 -15
- data/lib/lithop/settings_file.rb +1 -0
- data/lib/lithop/version.rb +1 -1
- data/lib/lithop/web/app.rb +9 -5
- data/lib/lithop/web/public/app.css +48 -37
- data/lib/lithop/web/public/app.js +149 -47
- data/lib/lithop/web/settings_form.rb +8 -5
- data/lib/lithop/web/views/index.erb +129 -75
- data/lib/lithop.rb +2 -1
- data/sig/lithop.rbs +16 -0
- metadata +3 -2
- data/lib/lithop/model/layer_region_model.rb +0 -24
|
@@ -3,6 +3,7 @@ const form = document.querySelector("#conversion-form");
|
|
|
3
3
|
const fileInput = document.querySelector("#image-input");
|
|
4
4
|
const filePicker = document.querySelector("#file-picker");
|
|
5
5
|
const fileName = document.querySelector("#file-name");
|
|
6
|
+
const submitButton = document.querySelector("#submit-button");
|
|
6
7
|
const previewIdInput = document.querySelector("#preview-id");
|
|
7
8
|
const sourceImage = document.querySelector("#source-image");
|
|
8
9
|
const compareSourceImage = document.querySelector("#compare-source-image");
|
|
@@ -29,9 +30,11 @@ const previewLabels = Object.fromEntries(previewKinds.map(({ name, label }) => [
|
|
|
29
30
|
const settingLabels = {
|
|
30
31
|
width: "幅", height: "高さ", pixel_size: "XYセル", color_pixel_size: "色セル",
|
|
31
32
|
image_fit: "配置", crop_position_x: "横位置", crop_position_y: "縦位置", crop_scale: "クロップ倍率",
|
|
32
|
-
brightness: "明るさ", contrast: "コントラスト", saturation: "彩度",
|
|
33
|
+
brightness: "明るさ", contrast: "コントラスト", saturation: "彩度", suppress_pale_colors: "淡色をWhite化",
|
|
34
|
+
yellow_mix_color: "Yellow混色", yellow_mix_percent: "Yellow混色率", pigment_order: "色の積層順",
|
|
35
|
+
tile_count: "タイル数",
|
|
33
36
|
tile_layout: "タイル配置", tile_rows: "行数", tile_columns: "列数", separate_z_parts: "Z分割",
|
|
34
|
-
color_region_parts: "
|
|
37
|
+
color_region_parts: "最大Zプレート数", minimum_thickness: "最小厚み", maximum_thickness: "最大厚み",
|
|
35
38
|
nozzle_diameter: "ノズル", margin_top: "上余白", margin_right: "右余白",
|
|
36
39
|
margin_bottom: "下余白", margin_left: "左余白"
|
|
37
40
|
};
|
|
@@ -71,6 +74,15 @@ const appViewLabels = { create: "作成", jobs: "ジョブ", presets: "プリセ
|
|
|
71
74
|
let currentView = requestedView();
|
|
72
75
|
let currentJobDetailTab = requestedJobTab();
|
|
73
76
|
|
|
77
|
+
function updateHeaderHeight() {
|
|
78
|
+
const height = document.querySelector(".site-header")?.getBoundingClientRect().height;
|
|
79
|
+
if (height) document.documentElement.style.setProperty("--site-header-height", `${Math.ceil(height)}px`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
updateHeaderHeight();
|
|
83
|
+
if (window.ResizeObserver) new ResizeObserver(updateHeaderHeight).observe(document.querySelector(".site-header"));
|
|
84
|
+
else window.addEventListener("resize", updateHeaderHeight);
|
|
85
|
+
|
|
74
86
|
function requestedView() {
|
|
75
87
|
const parameters = new URL(window.location.href).searchParams;
|
|
76
88
|
const view = parameters.get("view");
|
|
@@ -91,11 +103,6 @@ function viewUrl(view) {
|
|
|
91
103
|
return `/?${parameters}`;
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
function closeNavigation() {
|
|
95
|
-
document.body.classList.remove("navigation-open");
|
|
96
|
-
document.querySelector("#menu-toggle")?.setAttribute("aria-expanded", "false");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
106
|
function showAppView(view, { push = false, focus = false } = {}) {
|
|
100
107
|
if (!Object.hasOwn(appViewLabels, view)) view = "create";
|
|
101
108
|
currentView = view;
|
|
@@ -109,7 +116,6 @@ function showAppView(view, { push = false, focus = false } = {}) {
|
|
|
109
116
|
});
|
|
110
117
|
document.querySelector("#current-view-label").textContent = appViewLabels[view];
|
|
111
118
|
document.title = `${appViewLabels[view]} · Lithop Local UI`;
|
|
112
|
-
closeNavigation();
|
|
113
119
|
if (push) window.history.pushState({ jobId: currentJobId, view }, "", viewUrl(view));
|
|
114
120
|
if (focus) document.querySelector(`[data-app-view="${view}"] .view-title`)?.focus({ preventScroll: true });
|
|
115
121
|
window.requestAnimationFrame(() => viewerStates?.forEach?.((_state, id) => applyViewer(id)));
|
|
@@ -121,20 +127,31 @@ document.querySelectorAll("[data-view-target]").forEach((control) => {
|
|
|
121
127
|
showAppView(control.dataset.viewTarget, { push: currentView !== control.dataset.viewTarget, focus: true });
|
|
122
128
|
});
|
|
123
129
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
document.
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
|
|
131
|
+
function selectSettingsSection(id, { scroll = true, focus = true } = {}) {
|
|
132
|
+
const section = document.getElementById(id);
|
|
133
|
+
if (!section) return;
|
|
134
|
+
section.open = true;
|
|
135
|
+
document.querySelectorAll("[data-settings-target]").forEach((button) => {
|
|
136
|
+
if (button.dataset.settingsTarget === id) button.setAttribute("aria-current", "true");
|
|
137
|
+
else button.removeAttribute("aria-current");
|
|
138
|
+
});
|
|
139
|
+
if (scroll) section.scrollIntoView({ block: "start", behavior: "smooth" });
|
|
140
|
+
if (focus) window.setTimeout(() => section.querySelector("summary")?.focus({ preventScroll: true }), 0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
document.querySelectorAll("[data-settings-target]").forEach((button) => {
|
|
144
|
+
button.addEventListener("click", () => selectSettingsSection(button.dataset.settingsTarget));
|
|
129
145
|
});
|
|
130
|
-
document.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
146
|
+
document.querySelectorAll(".settings-section").forEach((section) => {
|
|
147
|
+
section.addEventListener("toggle", () => {
|
|
148
|
+
if (!section.open) return;
|
|
149
|
+
document.querySelectorAll("[data-settings-target]").forEach((button) => {
|
|
150
|
+
if (button.dataset.settingsTarget === section.id) button.setAttribute("aria-current", "true");
|
|
151
|
+
else button.removeAttribute("aria-current");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
136
154
|
});
|
|
137
|
-
|
|
138
155
|
function selectJobDetailTab(name, { focus = false, push = false } = {}) {
|
|
139
156
|
const button = document.querySelector(`[data-job-tab="${name}"]`);
|
|
140
157
|
if (!button) return;
|
|
@@ -231,14 +248,16 @@ function updateDraftSummary() {
|
|
|
231
248
|
const resolvedLayout = resolvedSummary
|
|
232
249
|
? `${resolvedSummary.tile_rows}行 × ${resolvedSummary.tile_columns}列`
|
|
233
250
|
: layoutLabels[values.tile_layout] || values.tile_layout;
|
|
251
|
+
const zPartCount = resolvedSummary?.z_plate_count || resolvedStandardZPartCount(values.color_region_parts);
|
|
234
252
|
const z = separateZParts?.checked
|
|
235
|
-
?
|
|
253
|
+
? `連続Z分割(${zPartCount}プレート・CMYW 8層・${zPartBaseWhiteSummary(zPartCount)}・最小${minimumZPlateThickness(zPartCount)} mm)`
|
|
236
254
|
: "一体出力";
|
|
237
255
|
const entries = [
|
|
238
256
|
["入力", fileInput.files?.[0]?.name || "未選択"],
|
|
239
257
|
["完成寸法", `${formatSetting(values.width)} × ${formatSetting(values.height)} mm`],
|
|
240
258
|
["セル", `XY ${formatSetting(values.pixel_size, " mm")} / 色 ${formatSetting(values.color_pixel_size, " mm")}`],
|
|
241
|
-
["画像", `${values.image_fit === "contain" ? "全体を収める" : "全面に配置"} / 明るさ ${values.brightness}% / コントラスト ${values.contrast}% / 彩度 ${values.saturation}%`],
|
|
259
|
+
["画像", `${values.image_fit === "contain" ? "全体を収める" : "全面に配置"} / 明るさ ${values.brightness}% / コントラスト ${values.contrast}% / 彩度 ${values.saturation}% / 淡色White化 ${values.suppress_pale_colors ? "有効" : "無効"} / Yellow→${values.yellow_mix_color === "magenta" ? "Magenta" : "Cyan"} ${values.yellow_mix_percent}%`],
|
|
260
|
+
["色積層", (values.pigment_order || "").split(",").map((pigment) => pigment[0]?.toUpperCase()).join("")],
|
|
242
261
|
["分割", `${values.tile_count}タイル・${resolvedLayout}(${layoutLabels[values.tile_layout] || values.tile_layout}) / ${z}`]
|
|
243
262
|
];
|
|
244
263
|
summary.replaceChildren();
|
|
@@ -277,9 +296,24 @@ function updateDraftState(message) {
|
|
|
277
296
|
else if (submittedSignature && submittedSignature !== settingsSignature()) state.textContent = "実行した設定から変更されています";
|
|
278
297
|
else state.textContent = "入力プレビューに反映済み";
|
|
279
298
|
updateDraftSummary();
|
|
299
|
+
updateStartAvailability();
|
|
280
300
|
if (displayedJob) updateJobContext(displayedJob);
|
|
281
301
|
}
|
|
282
302
|
|
|
303
|
+
function updateStartAvailability() {
|
|
304
|
+
if (!submitButton) return;
|
|
305
|
+
const hasFile = Boolean(fileInput.files?.[0]);
|
|
306
|
+
const previewFailed = hasFile && !stagePromise && !previewId;
|
|
307
|
+
submitButton.disabled = submitting || Boolean(activeJobId) || !hasFile || previewFailed;
|
|
308
|
+
let label = "変換を開始";
|
|
309
|
+
if (submitting) label = "変換を準備しています…";
|
|
310
|
+
else if (activeJobId) label = "変換中";
|
|
311
|
+
else if (!hasFile) label = "画像を選択してください";
|
|
312
|
+
else if (previewFailed) label = "画像を確認してください";
|
|
313
|
+
else if (stagePromise) label = "準備完了後に開始";
|
|
314
|
+
if (submitButton.textContent !== label) submitButton.textContent = label;
|
|
315
|
+
}
|
|
316
|
+
|
|
283
317
|
function showFormError(message) {
|
|
284
318
|
const error = document.querySelector("#form-error");
|
|
285
319
|
error.textContent = message;
|
|
@@ -413,6 +447,7 @@ function stageFile(file, { render = true } = {}) {
|
|
|
413
447
|
previewId = payload.id;
|
|
414
448
|
previewIdInput.value = previewId;
|
|
415
449
|
if (render) await renderPreview(previewId);
|
|
450
|
+
updateStartAvailability();
|
|
416
451
|
})
|
|
417
452
|
.catch((error) => {
|
|
418
453
|
if (requestNumber === selectionSequence) {
|
|
@@ -422,8 +457,10 @@ function stageFile(file, { render = true } = {}) {
|
|
|
422
457
|
})
|
|
423
458
|
.finally(() => {
|
|
424
459
|
if (stagePromise === promise) stagePromise = undefined;
|
|
460
|
+
updateStartAvailability();
|
|
425
461
|
});
|
|
426
462
|
stagePromise = promise;
|
|
463
|
+
updateStartAvailability();
|
|
427
464
|
return promise;
|
|
428
465
|
}
|
|
429
466
|
|
|
@@ -436,6 +473,7 @@ function selectFile(file) {
|
|
|
436
473
|
window.clearTimeout(renderTimer);
|
|
437
474
|
discardPreview(oldPreviewId);
|
|
438
475
|
showFormError("");
|
|
476
|
+
updateStartAvailability();
|
|
439
477
|
if (!file) {
|
|
440
478
|
fileName.textContent = "ファイルをここへ選択";
|
|
441
479
|
showSourceStatus("画像未選択");
|
|
@@ -504,24 +542,22 @@ function updateZFields() {
|
|
|
504
542
|
field.hidden = !visible;
|
|
505
543
|
input.disabled = !visible;
|
|
506
544
|
input.required = visible;
|
|
545
|
+
const partCount = resolvedStandardZPartCount(input.value);
|
|
507
546
|
document.querySelector("#z-parts-summary").textContent = visible
|
|
508
|
-
? `
|
|
547
|
+
? `CMYW 8層・${zPartBaseWhiteSummary(partCount)}付き・各プレート最小${minimumZPlateThickness(partCount)} mm(実出力 ${partCount}プレート)`
|
|
509
548
|
: "Z方向は一体のCMYWモデルとして出力します";
|
|
510
549
|
const diagram = document.querySelector("#z-parts-diagram");
|
|
511
550
|
diagram.replaceChildren();
|
|
512
551
|
if (visible) {
|
|
513
|
-
const
|
|
514
|
-
const size = Math.floor(7 / partCount);
|
|
515
|
-
let remainder = 7 % partCount;
|
|
516
|
-
let layer = 0;
|
|
552
|
+
const sourceWidth = 0.96 / partCount;
|
|
517
553
|
const labels = Array.from({ length: partCount }, (_value, index) => {
|
|
518
|
-
const
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
return
|
|
554
|
+
const start = 0.1 + (index * sourceWidth);
|
|
555
|
+
const finish = 0.1 + ((index + 1) * sourceWidth);
|
|
556
|
+
const endLabel = index === partCount - 1 ? `可変上面(最小 ${formatZMillimeters(finish)} mm)` : `${formatZMillimeters(finish)} mm`;
|
|
557
|
+
const baseWhite = zPartBaseWhiteThickness(partCount, index);
|
|
558
|
+
return `Zプレート ${index + 1}\nBase White ${formatZMillimeters(baseWhite)} mm + 元Z ${formatZMillimeters(start)} mm–${endLabel}`;
|
|
523
559
|
});
|
|
524
|
-
labels.
|
|
560
|
+
labels.forEach((label) => {
|
|
525
561
|
const part = document.createElement("span");
|
|
526
562
|
part.textContent = label;
|
|
527
563
|
diagram.append(part);
|
|
@@ -530,6 +566,31 @@ function updateZFields() {
|
|
|
530
566
|
diagram.hidden = !visible;
|
|
531
567
|
}
|
|
532
568
|
|
|
569
|
+
function resolvedStandardZPartCount(requested) {
|
|
570
|
+
return Math.min(Math.max(Number(requested) || 1, 1), 3);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function minimumZPlateThickness(partCount) {
|
|
574
|
+
const lastSourceStart = 0.1 + ((partCount - 1) * (0.96 / partCount));
|
|
575
|
+
return formatZMillimeters((0.82 - lastSourceStart) + zPartBaseWhiteThickness(partCount, partCount - 1));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function zPartBaseWhiteLayers(partCount, index) {
|
|
579
|
+
return partCount === 3 && index === 2 ? 3 : 1;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function zPartBaseWhiteThickness(partCount, index) {
|
|
583
|
+
return 0.1 + ((zPartBaseWhiteLayers(partCount, index) - 1) * 0.08);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function zPartBaseWhiteSummary(partCount) {
|
|
587
|
+
return partCount === 3 ? "Base White 1層(Zプレート3は3層)" : "Base White 1層";
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function formatZMillimeters(value) {
|
|
591
|
+
return value.toFixed(3).replace(/0+$/, "").replace(/\.$/, "");
|
|
592
|
+
}
|
|
593
|
+
|
|
533
594
|
function updateDimensions() {
|
|
534
595
|
const width = Number(document.querySelector("#width").value);
|
|
535
596
|
const height = Number(document.querySelector("#height").value);
|
|
@@ -546,6 +607,7 @@ updateGridFields();
|
|
|
546
607
|
updateZFields();
|
|
547
608
|
updateDimensions();
|
|
548
609
|
updateDraftSummary();
|
|
610
|
+
updateStartAvailability();
|
|
549
611
|
refreshResolvedSummary();
|
|
550
612
|
|
|
551
613
|
const widthInput = document.querySelector("#width");
|
|
@@ -599,23 +661,23 @@ form?.addEventListener("input", (event) => {
|
|
|
599
661
|
}
|
|
600
662
|
});
|
|
601
663
|
form?.addEventListener("change", (event) => {
|
|
602
|
-
|
|
664
|
+
const alreadyHandledByInput = ["number", "range", "text"].includes(event.target.type);
|
|
665
|
+
if (event.target !== fileInput && !alreadyHandledByInput) {
|
|
603
666
|
schedulePreview();
|
|
604
667
|
scheduleTileOverlay();
|
|
605
668
|
updateDraftState();
|
|
606
669
|
}
|
|
607
670
|
});
|
|
608
671
|
form?.addEventListener("invalid", (event) => {
|
|
609
|
-
event.target.closest("details")
|
|
672
|
+
const section = event.target.closest("details.settings-section");
|
|
673
|
+
if (section) selectSettingsSection(section.id, { scroll: false, focus: false });
|
|
610
674
|
const label = event.target.closest("label")?.childNodes?.[0]?.textContent?.trim();
|
|
611
675
|
showFormError(`${label || "入力値"}を確認してください`);
|
|
612
676
|
}, true);
|
|
613
677
|
|
|
614
678
|
function setSubmitting(value) {
|
|
615
679
|
submitting = value;
|
|
616
|
-
|
|
617
|
-
button.disabled = value;
|
|
618
|
-
button.textContent = value ? "変換を準備しています…" : "変換を開始";
|
|
680
|
+
updateStartAvailability();
|
|
619
681
|
}
|
|
620
682
|
|
|
621
683
|
form?.addEventListener("submit", async (event) => {
|
|
@@ -625,6 +687,9 @@ form?.addEventListener("submit", async (event) => {
|
|
|
625
687
|
showFormError("");
|
|
626
688
|
try {
|
|
627
689
|
if (stagePromise) await stagePromise;
|
|
690
|
+
if (!previewId) throw new Error("画像の準備に失敗しました。画像を選び直してください");
|
|
691
|
+
window.clearTimeout(renderTimer);
|
|
692
|
+
await renderPreview(previewId);
|
|
628
693
|
const data = new FormData(form);
|
|
629
694
|
if (previewId) {
|
|
630
695
|
data.delete("image");
|
|
@@ -673,7 +738,7 @@ function applySettings(values, sourceLabel) {
|
|
|
673
738
|
const detail = differences.length
|
|
674
739
|
? differences.map((difference) => `${difference.label}: ${difference.before} → ${difference.after}`).join("\n")
|
|
675
740
|
: "現在の設定との差はありません。";
|
|
676
|
-
if (!window.confirm(`${sourceLabel}
|
|
741
|
+
if (!window.confirm(`${sourceLabel}を作成に適用します。\n\n${detail}`)) return false;
|
|
677
742
|
Object.entries(settingLabels).forEach(([key]) => {
|
|
678
743
|
if (!(key in values)) return;
|
|
679
744
|
const control = form.elements.namedItem(key);
|
|
@@ -694,7 +759,12 @@ function applySettings(values, sourceLabel) {
|
|
|
694
759
|
document.querySelector("#settings-library-status").textContent = fileInput.files?.[0]
|
|
695
760
|
? `${sourceLabel}を反映しました。入力画像は変更していません。`
|
|
696
761
|
: `${sourceLabel}を反映しました。画像は保存されないため、選択してください。`;
|
|
697
|
-
showAppView("create", { push: currentView !== "create"
|
|
762
|
+
showAppView("create", { push: currentView !== "create" });
|
|
763
|
+
const state = document.querySelector("#draft-state");
|
|
764
|
+
window.setTimeout(() => {
|
|
765
|
+
state.scrollIntoView({ block: "center" });
|
|
766
|
+
state.focus({ preventScroll: true });
|
|
767
|
+
}, 0);
|
|
698
768
|
return true;
|
|
699
769
|
}
|
|
700
770
|
|
|
@@ -715,10 +785,16 @@ function renderSettingsLibrary() {
|
|
|
715
785
|
select.replaceChildren(new Option("プリセットを選択", ""));
|
|
716
786
|
Object.keys(settingsLibrary.presets || {}).forEach((name) => select.add(new Option(name, name)));
|
|
717
787
|
if (settingsLibrary.presets?.[selected]) select.value = selected;
|
|
788
|
+
updatePresetSelectionControls();
|
|
718
789
|
document.querySelector("#draft-restore").disabled = !settingsLibrary.draft;
|
|
719
790
|
document.querySelector("#draft-delete").disabled = !settingsLibrary.draft;
|
|
720
791
|
}
|
|
721
792
|
|
|
793
|
+
function updatePresetSelectionControls() {
|
|
794
|
+
const selected = document.querySelector("#preset-select").value;
|
|
795
|
+
document.querySelector("#preset-load").disabled = !settingsLibrary.presets?.[selected];
|
|
796
|
+
}
|
|
797
|
+
|
|
722
798
|
async function loadSettingsLibrary() {
|
|
723
799
|
try {
|
|
724
800
|
const response = await fetch("/api/settings-library", { cache: "no-store" });
|
|
@@ -732,6 +808,7 @@ async function loadSettingsLibrary() {
|
|
|
732
808
|
|
|
733
809
|
document.querySelector("#preset-select")?.addEventListener("change", (event) => {
|
|
734
810
|
document.querySelector("#preset-name").value = event.target.value;
|
|
811
|
+
updatePresetSelectionControls();
|
|
735
812
|
});
|
|
736
813
|
document.querySelector("#preset-save")?.addEventListener("click", async () => {
|
|
737
814
|
try {
|
|
@@ -740,6 +817,7 @@ document.querySelector("#preset-save")?.addEventListener("click", async () => {
|
|
|
740
817
|
values.set("name", name);
|
|
741
818
|
await settingsLibraryRequest("/api/settings-library/presets", values);
|
|
742
819
|
document.querySelector("#preset-select").value = name;
|
|
820
|
+
updatePresetSelectionControls();
|
|
743
821
|
document.querySelector("#settings-library-status").textContent = `「${name}」へ現在設定を保存しました。画像は保存していません。`;
|
|
744
822
|
} catch (error) { showFormError(error.message); }
|
|
745
823
|
});
|
|
@@ -829,6 +907,7 @@ function updateGlobalJob(job) {
|
|
|
829
907
|
const representedId = representedJob?.job_id || activeJobId || currentJobId;
|
|
830
908
|
if (!representedId) {
|
|
831
909
|
control.hidden = true;
|
|
910
|
+
updateStartAvailability();
|
|
832
911
|
return;
|
|
833
912
|
}
|
|
834
913
|
const state = representedJob?.state || "queued";
|
|
@@ -841,6 +920,7 @@ function updateGlobalJob(job) {
|
|
|
841
920
|
cancel.hidden = !["queued", "running", "cancelling"].includes(state);
|
|
842
921
|
cancel.disabled = state === "cancelling";
|
|
843
922
|
cancel.textContent = state === "cancelling" ? "停止要求中" : "停止";
|
|
923
|
+
updateStartAvailability();
|
|
844
924
|
}
|
|
845
925
|
|
|
846
926
|
function showJobPanel(jobId) {
|
|
@@ -947,7 +1027,7 @@ function renderTileModels(job) {
|
|
|
947
1027
|
const previews = new Map(job.artifacts.map((artifact) => [artifact.name, artifact]));
|
|
948
1028
|
const metadata = new Map((job.tiles || []).map((tile) => [tile.file, tile]));
|
|
949
1029
|
const plateCount = job.z_part_separation?.enabled
|
|
950
|
-
? (job.z_part_separation.
|
|
1030
|
+
? (job.z_part_separation.plates?.length || job.z_part_separation.resolved_parts || 0)
|
|
951
1031
|
: 1;
|
|
952
1032
|
list.replaceChildren();
|
|
953
1033
|
section.querySelector(".tile-map")?.remove();
|
|
@@ -1177,7 +1257,10 @@ function renderJob(job) {
|
|
|
1177
1257
|
if (job.job_id === activeJobId) {
|
|
1178
1258
|
activeJob = job;
|
|
1179
1259
|
updateGlobalJob(job);
|
|
1180
|
-
if (!active)
|
|
1260
|
+
if (!active) {
|
|
1261
|
+
activeJobId = undefined;
|
|
1262
|
+
updateStartAvailability();
|
|
1263
|
+
}
|
|
1181
1264
|
} else if (!activeJobId) {
|
|
1182
1265
|
updateGlobalJob(job);
|
|
1183
1266
|
}
|
|
@@ -1249,7 +1332,10 @@ async function pollJob(jobId) {
|
|
|
1249
1332
|
if (isActiveJob) {
|
|
1250
1333
|
activeJob = job;
|
|
1251
1334
|
updateGlobalJob(job);
|
|
1252
|
-
if (!active)
|
|
1335
|
+
if (!active) {
|
|
1336
|
+
activeJobId = undefined;
|
|
1337
|
+
updateStartAvailability();
|
|
1338
|
+
}
|
|
1253
1339
|
}
|
|
1254
1340
|
if (jobId === currentJobId) renderJob(job);
|
|
1255
1341
|
if (active && (isActiveJob || jobId === currentJobId)) pollTimer = window.setTimeout(() => pollJob(jobId), 900);
|
|
@@ -1318,9 +1404,15 @@ function updateHistorySelection() {
|
|
|
1318
1404
|
const selected = [...selectedJobIds].map((id) => historyJobs.get(id)).filter(Boolean);
|
|
1319
1405
|
const completed = selected.filter((job) => job.state === "completed");
|
|
1320
1406
|
const final = selected.filter((job) => ["completed", "failed", "cancelled"].includes(job.state));
|
|
1407
|
+
const selectable = [...historyJobs.values()].filter((job) => ["completed", "failed", "cancelled"].includes(job.state));
|
|
1408
|
+
const allSelected = selectable.length > 0 && selectable.every((job) => selectedJobIds.has(job.job_id));
|
|
1321
1409
|
const size = selected.reduce((sum, job) => sum + Number(job.size_bytes || 0), 0);
|
|
1322
1410
|
document.querySelector("#history-selection-status").textContent = selected.length
|
|
1323
1411
|
? `${selected.length}件選択・${formatBytes(size)}` : (historyNotice || "未選択");
|
|
1412
|
+
const selectAll = document.querySelector("#select-all-jobs");
|
|
1413
|
+
selectAll.disabled = selectable.length === 0;
|
|
1414
|
+
selectAll.setAttribute("aria-pressed", String(allSelected));
|
|
1415
|
+
selectAll.textContent = allSelected ? "全ての選択を解除する" : "全てを選択する";
|
|
1324
1416
|
document.querySelector("#compare-jobs").disabled = selected.length !== 2 || completed.length !== 2;
|
|
1325
1417
|
document.querySelector("#delete-selected-jobs").disabled = selected.length === 0 || final.length !== selected.length;
|
|
1326
1418
|
}
|
|
@@ -1446,6 +1538,19 @@ document.querySelector("#history-query")?.addEventListener("input", () => {
|
|
|
1446
1538
|
historySearchTimer = window.setTimeout(() => refreshHistory(), 250);
|
|
1447
1539
|
});
|
|
1448
1540
|
document.querySelector("#load-more-jobs")?.addEventListener("click", () => refreshHistory({ append: true }));
|
|
1541
|
+
document.querySelector("#select-all-jobs")?.addEventListener("click", () => {
|
|
1542
|
+
historyNotice = undefined;
|
|
1543
|
+
const selectable = [...historyJobs.values()].filter((job) => ["completed", "failed", "cancelled"].includes(job.state));
|
|
1544
|
+
const allSelected = selectable.length > 0 && selectable.every((job) => selectedJobIds.has(job.job_id));
|
|
1545
|
+
selectable.forEach((job) => {
|
|
1546
|
+
if (allSelected) selectedJobIds.delete(job.job_id);
|
|
1547
|
+
else selectedJobIds.add(job.job_id);
|
|
1548
|
+
});
|
|
1549
|
+
document.querySelectorAll("#job-list .history-select").forEach((checkbox) => {
|
|
1550
|
+
checkbox.checked = selectedJobIds.has(checkbox.dataset.jobId);
|
|
1551
|
+
});
|
|
1552
|
+
updateHistorySelection();
|
|
1553
|
+
});
|
|
1449
1554
|
|
|
1450
1555
|
document.querySelector("#delete-selected-jobs")?.addEventListener("click", async () => {
|
|
1451
1556
|
const jobs = [...selectedJobIds].map((id) => historyJobs.get(id)).filter(Boolean);
|
|
@@ -1571,7 +1676,6 @@ document.querySelector("#close-job-compare")?.addEventListener("click", () => do
|
|
|
1571
1676
|
const job = comparedJobs[index];
|
|
1572
1677
|
if (job && applySettings(job, `ジョブ ${job.job_id} の設定`)) {
|
|
1573
1678
|
document.querySelector("#job-compare-dialog").close();
|
|
1574
|
-
document.querySelector("#conversion-form").scrollIntoView({ block: "start" });
|
|
1575
1679
|
}
|
|
1576
1680
|
});
|
|
1577
1681
|
});
|
|
@@ -1622,9 +1726,7 @@ document.querySelector("#choose-another-image")?.addEventListener("click", () =>
|
|
|
1622
1726
|
});
|
|
1623
1727
|
document.querySelector("#review-settings")?.addEventListener("click", () => {
|
|
1624
1728
|
showAppView("create", { push: currentView !== "create" });
|
|
1625
|
-
|
|
1626
|
-
settings.open = true;
|
|
1627
|
-
settings.scrollIntoView({ block: "start" });
|
|
1729
|
+
selectSettingsSection("settings-basic", { focus: false });
|
|
1628
1730
|
document.querySelector("#width")?.focus();
|
|
1629
1731
|
});
|
|
1630
1732
|
document.querySelector("#retry-status-button")?.addEventListener("click", () => {
|
|
@@ -6,12 +6,13 @@ module Lithop
|
|
|
6
6
|
class SettingsForm
|
|
7
7
|
FLOAT_FIELDS = %i[
|
|
8
8
|
width height margin_top margin_right margin_bottom margin_left crop_position_x crop_position_y
|
|
9
|
-
crop_scale brightness contrast saturation minimum_thickness maximum_thickness pixel_size
|
|
9
|
+
crop_scale brightness contrast saturation yellow_mix_percent minimum_thickness maximum_thickness pixel_size
|
|
10
10
|
color_pixel_size nozzle_diameter
|
|
11
11
|
].freeze
|
|
12
12
|
INTEGER_FIELDS = %i[tile_count tile_rows tile_columns color_region_parts].freeze
|
|
13
|
-
STRING_FIELDS = %i[image_fit tile_layout].freeze
|
|
14
|
-
|
|
13
|
+
STRING_FIELDS = %i[image_fit tile_layout yellow_mix_color pigment_order].freeze
|
|
14
|
+
BOOLEAN_FIELDS = %i[suppress_pale_colors separate_z_parts].freeze
|
|
15
|
+
FIELD_NAMES = (FLOAT_FIELDS + INTEGER_FIELDS + STRING_FIELDS + BOOLEAN_FIELDS).map(&:to_s).freeze
|
|
15
16
|
|
|
16
17
|
attr_reader :defaults
|
|
17
18
|
|
|
@@ -27,7 +28,9 @@ module Lithop
|
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def values(settings = defaults)
|
|
30
|
-
settings.to_h.slice(*Settings::DEFAULTS.keys).transform_keys(&:to_s)
|
|
31
|
+
result = settings.to_h.slice(*Settings::DEFAULTS.keys).transform_keys(&:to_s)
|
|
32
|
+
result["pigment_order"] = settings.pigment_order.join(",")
|
|
33
|
+
result
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
private
|
|
@@ -37,7 +40,7 @@ module Lithop
|
|
|
37
40
|
FLOAT_FIELDS.each { |key| assign_number(values, params, key, Float) }
|
|
38
41
|
INTEGER_FIELDS.each { |key| assign_number(values, params, key, Integer) }
|
|
39
42
|
STRING_FIELDS.each { |key| values[key] = params[key.to_s] if present?(params[key.to_s]) }
|
|
40
|
-
values[
|
|
43
|
+
BOOLEAN_FIELDS.each { |key| values[key] = params[key.to_s] == "1" }
|
|
41
44
|
clear_unused_grid(values)
|
|
42
45
|
values
|
|
43
46
|
rescue ArgumentError
|