three-slicer 0.1.2 → 0.1.3
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.
- package/data/config-schema.json +999 -900
- package/data/invalidation-map.json +5 -4
- package/data/printers.json +1 -0
- package/data/processes.js +2 -0
- package/data/toggle-rules.json +44 -44
- package/data/ui-tree.json +170 -494
- package/engine/src/data.js +7 -1
- package/engine/src/settings.js +86 -1
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.js +0 -0
- package/package.json +4 -2
- package/types/data/printers.json.d.ts +23 -0
- package/types/data/processes.js.d.ts +21 -0
- package/types/data.d.ts +5 -2
- package/types/settings-keys.d.ts +14 -3
- package/viewer/styles.css +21 -0
package/engine/src/data.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The single import site for the
|
|
1
|
+
// The single import site for the extracted JSON files.
|
|
2
2
|
//
|
|
3
3
|
// Why funnel them through one module: JSON requires `with { type: 'json' }` in Node 22+ ESM, but
|
|
4
4
|
// Vite/esbuild strip that attribute from bundle output (neither build.target nor importAttributesKey
|
|
@@ -10,3 +10,9 @@ export { default as schema } from 'three-slicer/data/config-schema.json' with {
|
|
|
10
10
|
export { default as uiTree } from 'three-slicer/data/ui-tree.json' with { type: 'json' }
|
|
11
11
|
export { default as toggleRules } from 'three-slicer/data/toggle-rules.json' with { type: 'json' }
|
|
12
12
|
export { default as invalidationMap } from 'three-slicer/data/invalidation-map.json' with { type: 'json' }
|
|
13
|
+
export { default as printers } from 'three-slicer/data/printers.json' with { type: 'json' }
|
|
14
|
+
// processes is the largest artifact (~800 KB) and is only needed once a printer is picked, so it stays a dynamic
|
|
15
|
+
// import. It is generated as a JS module rather than JSON precisely because of the attribute problem above, in its
|
|
16
|
+
// other direction: a dynamic JSON import needs the attribute in Node, and that same attribute makes browsers
|
|
17
|
+
// reject the text/javascript response a dev server gives for .json.
|
|
18
|
+
export const loadProcesses = () => import('three-slicer/data/processes.js').then(m => m.default)
|
package/engine/src/settings.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Maps the actual values of the (editable) right-hand settings panel -> kernel slice parameters.
|
|
2
2
|
// - Settings state is a sparse map (key->value): only edited keys are stored, otherwise the config-schema default.
|
|
3
3
|
// - For vector types (coFloats/coInts, …) only the first element is used/edited (simplification).
|
|
4
|
-
import { schema } from './data.js'
|
|
4
|
+
import { schema, printers, loadProcesses } from './data.js'
|
|
5
5
|
|
|
6
6
|
export function schemaDefault(key) { return schema[key]?.default }
|
|
7
7
|
export function settingRaw(settings, key) { return (settings && key in settings) ? settings[key] : schemaDefault(key) }
|
|
@@ -13,6 +13,82 @@ const KERNEL_PATTERNS = ['rectilinear', 'grid', 'triangles', 'zigzag', 'gyroid',
|
|
|
13
13
|
'honeycomb', '3dhoneycomb', 'crosshatch', 'concentric']
|
|
14
14
|
const KERNEL_SEAMS = ['nearest', 'aligned', 'back', 'random']
|
|
15
15
|
|
|
16
|
+
// Machine limits (the "Motion ability" printer page) -> kernel time-estimate parameters.
|
|
17
|
+
// Declarative on purpose: the kernel collapses X/Y into one axis, so the mapping cannot be a plain pass-through,
|
|
18
|
+
// but adding a limit stays a data row instead of code. The fallback is the kernel's own default, so an unedited
|
|
19
|
+
// profile produces exactly the previous estimate. Upstream leaves machine_max_*_x/y/z/e without a schema default
|
|
20
|
+
// (they only ever come from a printer preset), hence the explicit fallbacks here.
|
|
21
|
+
const MACHINE_LIMITS = {
|
|
22
|
+
machine_max_speed_xy: ['machine_max_speed_x', 500],
|
|
23
|
+
machine_max_speed_z: ['machine_max_speed_z', 12],
|
|
24
|
+
machine_max_speed_e: ['machine_max_speed_e', 30],
|
|
25
|
+
machine_max_accel_xy: ['machine_max_acceleration_x', 5000],
|
|
26
|
+
machine_max_accel_z: ['machine_max_acceleration_z', 500],
|
|
27
|
+
machine_max_accel_e: ['machine_max_acceleration_e', 5000],
|
|
28
|
+
machine_jerk_xy: ['machine_max_jerk_x', 9],
|
|
29
|
+
machine_jerk_z: ['machine_max_jerk_z', 0.4],
|
|
30
|
+
machine_jerk_e: ['machine_max_jerk_e', 2.5],
|
|
31
|
+
machine_accel_print: ['default_acceleration', 5000],
|
|
32
|
+
machine_accel_travel: ['travel_acceleration', 5000],
|
|
33
|
+
machine_accel_retract: ['machine_max_acceleration_retracting', 5000],
|
|
34
|
+
}
|
|
35
|
+
// The schema keys the machine limits are read from — for UI that has to show "which printer characteristics are in play"
|
|
36
|
+
// without hardcoding key strings of its own.
|
|
37
|
+
export const machineLimitKeys = Object.values(MACHINE_LIMITS).map(([key]) => key)
|
|
38
|
+
|
|
39
|
+
// ---- Printer profiles -------------------------------------------------------
|
|
40
|
+
// printers.json is stored column-oriented (see its .d.ts). These two hide that layout so no consumer decodes it.
|
|
41
|
+
|
|
42
|
+
/** Every option key a printer profile can set — what to clear before applying a different printer. */
|
|
43
|
+
export const printerKeys = printers.keys
|
|
44
|
+
|
|
45
|
+
/** Vendor -> profile name -> `[nozzle, setIndex, model]`, straight from the data (for building a picker). */
|
|
46
|
+
export const printersByVendor = printers.byVendor
|
|
47
|
+
|
|
48
|
+
// Process (print) presets live in the ~800 KB processes.json, so they load on demand — the first call fetches,
|
|
49
|
+
// later ones reuse the same promise. Returns a small facade so no caller has to know the column layout.
|
|
50
|
+
let processesPromise = null
|
|
51
|
+
export function processPresets() {
|
|
52
|
+
processesPromise ??= loadProcesses().then(data => ({
|
|
53
|
+
/** Every key a process preset can set — clear these before applying a different one */
|
|
54
|
+
keys: data.keys,
|
|
55
|
+
/** Preset names compatible with a printer profile, in upstream order */
|
|
56
|
+
listFor: (printerProfileName) =>
|
|
57
|
+
(data.byPrinter[printerProfileName] ?? []).map(i => data.presets[i][0]),
|
|
58
|
+
/** The settings a preset applies, ready to merge. `null` when unknown. */
|
|
59
|
+
settingsFor: (presetName) => {
|
|
60
|
+
const preset = data.presets.find(([name]) => name === presetName)
|
|
61
|
+
if (!preset) return null
|
|
62
|
+
const row = data.sets[preset[1]], out = {}
|
|
63
|
+
data.keys.forEach((key, i) => { if (row[i] != null) out[key] = row[i] })
|
|
64
|
+
return out
|
|
65
|
+
},
|
|
66
|
+
}))
|
|
67
|
+
return processesPromise
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function printerEntry(profileName) {
|
|
71
|
+
for (const models of Object.values(printers.byVendor)) {
|
|
72
|
+
const entry = models[profileName]
|
|
73
|
+
if (entry) return entry
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The settings a printer profile applies, ready to merge into the settings map. `null` when unknown. */
|
|
79
|
+
export function printerSettings(profileName) {
|
|
80
|
+
const entry = printerEntry(profileName)
|
|
81
|
+
if (!entry) return null
|
|
82
|
+
const row = printers.sets[entry[1]]
|
|
83
|
+
const out = {}
|
|
84
|
+
printers.keys.forEach((key, i) => { if (row[i] != null) out[key] = row[i] })
|
|
85
|
+
return out
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The vendor's recommended process preset for a printer, or '' when the profile names none. */
|
|
89
|
+
export function printerDefaultPreset(profileName) { return printerEntry(profileName)?.[3] ?? '' }
|
|
90
|
+
|
|
91
|
+
|
|
16
92
|
// Right-panel settings values -> kernel parameters (derived from schema keys)
|
|
17
93
|
export function deriveKernelParams(settings) {
|
|
18
94
|
const S = k => settingScalar(settings, k)
|
|
@@ -49,8 +125,12 @@ export function deriveKernelParams(settings) {
|
|
|
49
125
|
'sparse_infill_line_width','internal_solid_infill_line_width','initial_layer_line_width']) {
|
|
50
126
|
const v = S(k); if (v != null && v !== '') widths[k] = v
|
|
51
127
|
}
|
|
128
|
+
const machine = {}
|
|
129
|
+
for (const [param, [key, fallback]] of Object.entries(MACHINE_LIMITS)) machine[param] = num(key, fallback)
|
|
130
|
+
|
|
52
131
|
return {
|
|
53
132
|
...widths,
|
|
133
|
+
...machine,
|
|
54
134
|
layer_height: num('layer_height', 0.2),
|
|
55
135
|
first_layer_height: num('initial_layer_print_height', 0.2),
|
|
56
136
|
line_width,
|
|
@@ -80,6 +160,11 @@ export function deriveKernelParams(settings) {
|
|
|
80
160
|
nozzle_temp: num('nozzle_temperature', 200),
|
|
81
161
|
bed_temp: num('hot_plate_temp', 45), // no bed_temperature key -> hot_plate_temp
|
|
82
162
|
bed_width, bed_depth,
|
|
163
|
+
bed_height: num('printable_height', 0), // 0 = profile states no ceiling -> kernel skips the check
|
|
164
|
+
// Explicitly-set only: the schema default here is upstream's generic "G28 / G1 Z5" preamble, and falling back
|
|
165
|
+
// to it would rewrite the emitted G-code for every caller. The kernel keeps its own preamble when these are empty.
|
|
166
|
+
machine_start_gcode: String(settings?.machine_start_gcode ?? ''),
|
|
167
|
+
machine_end_gcode: String(settings?.machine_end_gcode ?? ''),
|
|
83
168
|
enable_support: bool('enable_support', false),
|
|
84
169
|
support_threshold_angle: num('support_threshold_angle', 30),
|
|
85
170
|
support_top_z_distance: num('support_top_z_distance', 0.2),
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-slicer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Browser/WASM 3D-printing slicer reverse-engineered from OrcaSlicer: slicing engine (Arachne walls, tree/grid supports, multi-material), React viewer with GPU toolpath preview, and schema-driven settings UI.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -78,6 +78,8 @@
|
|
|
78
78
|
"data/ui-tree.json",
|
|
79
79
|
"data/toggle-rules.json",
|
|
80
80
|
"data/invalidation-map.json",
|
|
81
|
+
"data/printers.json",
|
|
82
|
+
"data/processes.js",
|
|
81
83
|
"README.md",
|
|
82
84
|
"LICENSE.txt"
|
|
83
85
|
],
|
|
@@ -86,7 +88,7 @@
|
|
|
86
88
|
"url": "git+https://github.com/kimgh06/Web_Three_Slicer.git",
|
|
87
89
|
"directory": "packages"
|
|
88
90
|
},
|
|
89
|
-
"homepage": "https://
|
|
91
|
+
"homepage": "https://slicer.kimgh06.com",
|
|
90
92
|
"keywords": [
|
|
91
93
|
"3d-printing",
|
|
92
94
|
"slicer",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// printers.json — per-printer settings extracted from OrcaSlicer's resources/profiles/<Vendor>/machine/.
|
|
2
|
+
// Column layout: `keys` names the columns once and each entry in `sets` is a positional row, because
|
|
3
|
+
// repeating the key names per printer costs more than all the values together. Rows are deduplicated —
|
|
4
|
+
// a printer's nozzle variants usually share one row — so a printer holds an index rather than its own copy.
|
|
5
|
+
// Prefer `printerSettings()` from three-slicer/settings over decoding this by hand.
|
|
6
|
+
|
|
7
|
+
/** A positional row aligned to `keys`; `null` where the profile chain never set that option. */
|
|
8
|
+
export type PrinterRow = (unknown | null)[]
|
|
9
|
+
|
|
10
|
+
/** `[nozzle diameter, index into sets, model name without the nozzle suffix]` */
|
|
11
|
+
export type PrinterEntry = [string, number, string]
|
|
12
|
+
|
|
13
|
+
export interface PrinterData {
|
|
14
|
+
/** Option keys, in column order — the same keys the settings panel edits */
|
|
15
|
+
keys: string[]
|
|
16
|
+
/** Deduplicated value rows */
|
|
17
|
+
sets: PrinterRow[]
|
|
18
|
+
/** Vendor -> printer profile name -> entry */
|
|
19
|
+
byVendor: Record<string, Record<string, PrinterEntry>>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare const printers: PrinterData
|
|
23
|
+
export default printers
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// processes.js — print (process) presets extracted from OrcaSlicer's resources/profiles/<Vendor>/process/.
|
|
2
|
+
// Emitted as a JS module rather than JSON because it is loaded dynamically: see engine/src/data.js for why.
|
|
3
|
+
// Column layout, same as printers.json — `keys` names the columns once and each row in `sets` is positional.
|
|
4
|
+
// Prefer `processPresets()` from three-slicer/settings over decoding this by hand.
|
|
5
|
+
|
|
6
|
+
/** A positional row aligned to `keys`; `null` where the profile chain never set that option. */
|
|
7
|
+
export type ProcessRow = (unknown | null)[]
|
|
8
|
+
|
|
9
|
+
export interface ProcessData {
|
|
10
|
+
/** Option keys, in column order — the keys the kernel consumes, read out of engine/src/settings.js */
|
|
11
|
+
keys: string[]
|
|
12
|
+
/** Deduplicated value rows */
|
|
13
|
+
sets: ProcessRow[]
|
|
14
|
+
/** `[preset name, index into sets]`, indexed by the numbers in `byPrinter` */
|
|
15
|
+
presets: [string, number][]
|
|
16
|
+
/** Printer profile name -> indices into `presets` */
|
|
17
|
+
byPrinter: Record<string, number[]>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
declare const processes: ProcessData
|
|
21
|
+
export default processes
|
package/types/data.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
// three-slicer/data — the
|
|
1
|
+
// three-slicer/data — the extracted JSON files. Prefer this over importing the raw JSON:
|
|
2
2
|
// the import attribute (`with { type: 'json' }`) is already applied, so it works in Node and bundlers alike.
|
|
3
3
|
import type { ConfigOption } from './data/config-schema.json.d.ts'
|
|
4
4
|
import type { UIPage } from './data/ui-tree.json.d.ts'
|
|
5
5
|
import type { ToggleGroup } from './data/toggle-rules.json.d.ts'
|
|
6
6
|
import type { InvalidationRule } from './data/invalidation-map.json.d.ts'
|
|
7
|
+
import type { PrinterData, PrinterRow, PrinterEntry } from './data/printers.json.d.ts'
|
|
7
8
|
|
|
8
|
-
export type { ConfigOption, UIPage, ToggleGroup, InvalidationRule }
|
|
9
|
+
export type { ConfigOption, UIPage, ToggleGroup, InvalidationRule, PrinterData, PrinterRow, PrinterEntry }
|
|
9
10
|
|
|
10
11
|
/** The 907 option definitions */
|
|
11
12
|
export const schema: Record<string, ConfigOption>
|
|
@@ -15,3 +16,5 @@ export const uiTree: Record<string, UIPage[]>
|
|
|
15
16
|
export const toggleRules: Record<string, ToggleGroup>
|
|
16
17
|
/** Setting key change -> invalidated steps */
|
|
17
18
|
export const invalidationMap: Record<string, InvalidationRule[]>
|
|
19
|
+
/** Machine limits per printer, from the upstream vendor profiles */
|
|
20
|
+
export const printers: PrinterData
|
package/types/settings-keys.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// AUTO-GENERATED by types/gen_settings_types.mjs — DO NOT EDIT.
|
|
2
|
-
// Source: data/config-schema.json (
|
|
2
|
+
// Source: data/config-schema.json (906 keys)
|
|
3
3
|
|
|
4
4
|
/** [x, y] — coPoint family */
|
|
5
5
|
export type Point = [number, number]
|
|
@@ -831,6 +831,7 @@ export interface SlicerSettings {
|
|
|
831
831
|
machine_hotend_change_time?: number
|
|
832
832
|
/** Filament load time — (s) — Time to load new filament when switch filament. It's usually applicable for single-extruder multi-material machines. For tool changers or multi-tool machines, it's typically 0. For statistics only. */
|
|
833
833
|
machine_load_filament_time?: number
|
|
834
|
+
/** (mm/s²) — Maximum acceleration of the E axis */
|
|
834
835
|
machine_max_acceleration_e?: number[] | number
|
|
835
836
|
/** (mm/s²) — Maximum acceleration for extruding (M204 P) */
|
|
836
837
|
machine_max_acceleration_extruding?: number[] | number
|
|
@@ -838,22 +839,33 @@ export interface SlicerSettings {
|
|
|
838
839
|
machine_max_acceleration_retracting?: number[] | number
|
|
839
840
|
/** (mm/s²) — Maximum acceleration for travel (M204 T), it only applies to Marlin 2. */
|
|
840
841
|
machine_max_acceleration_travel?: number[] | number
|
|
842
|
+
/** (mm/s²) — Maximum acceleration of the X axis */
|
|
841
843
|
machine_max_acceleration_x?: number[] | number
|
|
844
|
+
/** (mm/s²) — Maximum acceleration of the Y axis */
|
|
842
845
|
machine_max_acceleration_y?: number[] | number
|
|
846
|
+
/** (mm/s²) — Maximum acceleration of the Z axis */
|
|
843
847
|
machine_max_acceleration_z?: number[] | number
|
|
844
848
|
/** (N) — The allowed maximum output force of Y axis */
|
|
845
849
|
machine_max_force_Y?: number
|
|
850
|
+
/** (mm/s) — Maximum jerk of the E axis */
|
|
846
851
|
machine_max_jerk_e?: number[] | number
|
|
852
|
+
/** (mm/s) — Maximum jerk of the X axis */
|
|
847
853
|
machine_max_jerk_x?: number[] | number
|
|
854
|
+
/** (mm/s) — Maximum jerk of the Y axis */
|
|
848
855
|
machine_max_jerk_y?: number[] | number
|
|
856
|
+
/** (mm/s) — Maximum jerk of the Z axis */
|
|
849
857
|
machine_max_jerk_z?: number[] | number
|
|
850
858
|
/** (mm) — Maximum junction deviation (M205 J, only apply if JD > 0 for Marlin Firmware If your Marlin 2 printer uses Classic Jerk set this value to 0.) */
|
|
851
859
|
machine_max_junction_deviation?: number[] | number
|
|
852
860
|
/** (g) — The allowed max printed mass on a plate */
|
|
853
861
|
machine_max_printed_mass?: number
|
|
862
|
+
/** (mm/s) — Maximum speed of E axis */
|
|
854
863
|
machine_max_speed_e?: number[] | number
|
|
864
|
+
/** (mm/s) — Maximum speed of X axis */
|
|
855
865
|
machine_max_speed_x?: number[] | number
|
|
866
|
+
/** (mm/s) — Maximum speed of Y axis */
|
|
856
867
|
machine_max_speed_y?: number[] | number
|
|
868
|
+
/** (mm/s) — Maximum speed of Z axis */
|
|
857
869
|
machine_max_speed_z?: number[] | number
|
|
858
870
|
/** (mm/s) — Minimum speed for extruding (M205 S) */
|
|
859
871
|
machine_min_extruding_rate?: number[] | number
|
|
@@ -1056,8 +1068,6 @@ export interface SlicerSettings {
|
|
|
1056
1068
|
physical_printer_preset?: string
|
|
1057
1069
|
/** Send progress to pipe — Send progress to pipe. */
|
|
1058
1070
|
pipe?: string
|
|
1059
|
-
/** Capabilities — Configuration for the plugin capabilities this preset uses, overriding the global Capabilities configuration. Stored as a raw JSON array and edited through the dialog behind the button, never typed in directly. */
|
|
1060
|
-
plugin_config_overrides?: string
|
|
1061
1071
|
/** Plugins Used — Plugin capabilities referenced by this preset, stored as name;uuid;capability. */
|
|
1062
1072
|
plugins?: string[] | string
|
|
1063
1073
|
/** Position — Position of the extruder at the beginning of the custom G-code block. If the custom G-code travels somewhere else, it should write to this variable so OrcaSlicer knows where it travels from when it gets control back. */
|
|
@@ -1074,6 +1084,7 @@ export interface SlicerSettings {
|
|
|
1074
1084
|
preheat_steps?: number
|
|
1075
1085
|
/** Preheat time — (s) — To reduce the waiting time after tool change, Orca can preheat the next tool while the current tool is still in use. This setting specifies the time in seconds to preheat the next tool. Orca will insert a M104 command to preheat the tool in advance. */
|
|
1076
1086
|
preheat_time?: number
|
|
1087
|
+
/** Capabilities — Configuration for the plugin capabilities this preset uses, overriding the global Capabilities configuration. Stored as a raw JSON array and edited through the dialog behind the button, never typed in directly. */
|
|
1077
1088
|
preset_name?: string
|
|
1078
1089
|
/** Printer preset names — Names of presets related to the physical printer. */
|
|
1079
1090
|
preset_names?: string[] | string
|
package/viewer/styles.css
CHANGED
|
@@ -147,6 +147,27 @@
|
|
|
147
147
|
text-shadow: 0 1px 2px #000; pointer-events: none; }
|
|
148
148
|
/* ---- Slice status display (viewport overlay) ---- */
|
|
149
149
|
.slice-warn { color: #f0c060; margin: 4px 0; }
|
|
150
|
+
/* Basis line under the print-time estimate — the machine limits the kernel reported using */
|
|
151
|
+
.sc-sub { font-size: 10.5px; opacity: .62; margin: 1px 0 3px; }
|
|
152
|
+
/* Collapsible card section holding an injected settings page (printer motion ability) */
|
|
153
|
+
.sc-fold > summary { display: flex; justify-content: space-between; align-items: center; font-size: 12px;
|
|
154
|
+
color: #5a6069; padding: 2px 0; cursor: pointer; list-style: none; }
|
|
155
|
+
.sc-fold > summary::-webkit-details-marker { display: none; }
|
|
156
|
+
.sc-fold-lbl::before { content: '▸'; display: inline-block; width: 10px; font-size: 9px; opacity: .5; }
|
|
157
|
+
.sc-fold[open] .sc-fold-lbl::before { content: '▾'; }
|
|
158
|
+
.sc-fold > summary b { color: #24282e; }
|
|
159
|
+
.sc-fold[open] > summary { border-bottom: 1px solid #e3e6ea; margin-bottom: 4px; }
|
|
160
|
+
/* The injected page is a full settings form — keep it from stretching the card unboundedly */
|
|
161
|
+
.sc-fold-body { max-height: 320px; overflow-y: auto; }
|
|
162
|
+
.sc-model { max-width: 62%; font-size: 11.5px; padding: 1px 2px; }
|
|
163
|
+
/* Support helpers grouped under the "Generate support" toggle */
|
|
164
|
+
.support-tools { display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
165
|
+
margin: 2px 0 2px 16px; padding-left: 6px; border-left: 2px solid #e3e6ea; }
|
|
166
|
+
.sup-style { margin-left: 16px; padding-left: 6px; border-left: 2px solid #e3e6ea; }
|
|
167
|
+
.sup-paint { font-size: 11px; padding: 2px 8px; border: 1px solid #c9ced6; border-radius: 5px;
|
|
168
|
+
background: #fff; color: #24282e; cursor: pointer; white-space: nowrap; }
|
|
169
|
+
.sup-paint.on { background: #2b6cff; border-color: #2b6cff; color: #fff; }
|
|
170
|
+
.sup-paint:disabled { opacity: .45; cursor: not-allowed; }
|
|
150
171
|
.slice-btn { width: 100%; margin-top: 6px; padding: 7px; border: 0; border-radius: 7px;
|
|
151
172
|
background: #00ae42; color: #fff; font-weight: 700; cursor: pointer; }
|
|
152
173
|
.slice-btn:disabled { background: #3a4650; color: #8b98a2; cursor: not-allowed; }
|