diff --git a/scripts/detail_daemon.py b/scripts/detail_daemon.py index 8a05ab7..f555bcd 100644 --- a/scripts/detail_daemon.py +++ b/scripts/detail_daemon.py @@ -8,30 +8,66 @@ matplotlib.use('Agg') import matplotlib.pyplot as plt import modules.scripts as scripts -from modules.script_callbacks import on_cfg_denoiser, remove_callbacks_for_function, on_infotext_pasted +from modules.script_callbacks import on_cfg_denoiser, remove_callbacks_for_function, on_infotext_pasted, on_ui_settings from modules.ui_components import InputAccordion +from modules import shared +try: + import modules_forge.forge_version + is_forge = True +except: + is_forge = False + +def add_settings(): + section = ('detail_daemon', "Detail Daemon") + shared.opts.add_option("detail_daemon_count", shared.OptionInfo( + 6, "Daemon count", gr.Slider, {"minimum": 1, "maximum": 10, "step": 1}, section=section).needs_reload_ui()) + shared.opts.add_option("detail_daemon_verbose", shared.OptionInfo( + False, "Verbose", gr.Checkbox, {"interactive": True}, section=section)) + +on_ui_settings(add_settings) def parse_infotext(infotext, params): try: - d = {} - for s in params['Detail Daemon'].split(','): - k, _, v = s.partition(':') - d[k.strip()] = v.strip() - params['Detail Daemon'] = d + raw = params['Detail Daemon'] + if raw.startswith("D"): + daemons = raw.split(";") + if len(daemons) > shared.opts.data.get("detail_daemon_count", 6): + tqdm.write(f"\033[31mDetail Daemon:\033[0m Need more daemons! Go to 'Settings > Uncategorized > Detail Daemon' and increase count to {len(daemons)}.") + dd_dict = {} + for idx, daemon in enumerate(daemons): + tag, values = daemon.split(":") + vals = values.split(",") + dd_dict[f"active{idx + 1 if idx > 0 else ''}"] = bool(int(vals[0])) + dd_dict[f"hr{idx + 1 if idx > 0 else ''}"] = bool(int(vals[1])) + dd_dict[f"mode{idx + 1 if idx > 0 else ''}"] = vals[2] + dd_dict[f"amount{idx + 1 if idx > 0 else ''}"] = float(vals[3]) + dd_dict[f"st{idx + 1 if idx > 0 else ''}"] = float(vals[4]) + dd_dict[f"ed{idx + 1 if idx > 0 else ''}"] = float(vals[5]) + dd_dict[f"bias{idx + 1 if idx > 0 else ''}"] = float(vals[6]) + dd_dict[f"exp{idx + 1 if idx > 0 else ''}"] = float(vals[7]) + dd_dict[f"st_offset{idx + 1 if idx > 0 else ''}"] = float(vals[8]) + dd_dict[f"ed_offset{idx + 1 if idx > 0 else ''}"] = float(vals[9]) + dd_dict[f"fade{idx + 1 if idx > 0 else ''}"] = float(vals[10]) + dd_dict[f"smooth{idx + 1 if idx > 0 else ''}"] = bool(int(vals[11])) + params['Detail Daemon'] = dd_dict + else: + # fallback to old format for backward compatibility + d = {} + for s in raw.split(','): + k, _, v = s.partition(':') + d[k.strip()] = v.strip() + params['Detail Daemon'] = d except Exception: pass - on_infotext_pasted(parse_infotext) class Script(scripts.Script): def __init__(self): - super().__init__() - self.schedule_params: dict[str, float] = None - self.schedule = None + self.tab_param_count = 0 def title(self): return "Detail Daemon" @@ -40,155 +76,235 @@ class Script(scripts.Script): return scripts.AlwaysVisible def ui(self, is_img2img): - with InputAccordion(False, label="Detail Daemon", elem_id=self.elem_id('detail-daemon')) as gr_enabled: - with gr.Row(): - with gr.Column(scale=2): - gr_amount_slider = gr.Slider(minimum=-1.00, maximum=1.00, step=.01, value=0.10, label="Detail Amount") - gr_start = gr.Slider(minimum=0.0, maximum=1.0, step=.01, value=0.2, label="Start") - gr_end = gr.Slider(minimum=0.0, maximum=1.0, step=.01, value=0.8, label="End") - gr_bias = gr.Slider(minimum=0.0, maximum=1.0, step=.01, value=0.5, label="Bias") - with gr.Column(scale=1, min_width=275): - preview = self.visualize(False, 0.2, 0.8, 0.5, 0.1, 1, 0, 0, 0, True) - gr_vis = gr.Plot(value=preview, elem_classes=['detail-daemon-vis'], show_label=False) - with gr.Accordion("More Knobs:", elem_classes=['detail-daemon-more-accordion'], open=False): - with gr.Row(): - with gr.Column(scale=2): - with gr.Row(): - gr_start_offset_slider = gr.Slider(minimum=-1.00, maximum=1.00, step=.01, value=0.00, label="Start Offset", min_width=60) - gr_end_offset_slider = gr.Slider(minimum=-1.00, maximum=1.00, step=.01, value=0.00, label="End Offset", min_width=60) - with gr.Row(): - gr_exponent = gr.Slider(minimum=0.0, maximum=10.0, step=.05, value=1.0, label="Exponent", min_width=60) - gr_fade = gr.Slider(minimum=0.0, maximum=1.0, step=.05, value=0.0, label="Fade", min_width=60) - # Because the slider max and min are sometimes too limiting: - with gr.Row(): - gr_amount = gr.Number(value=0.10, precision=4, step=.01, label="Amount", min_width=60) - gr_start_offset = gr.Number(value=0.0, precision=4, step=.01, label="Start Offset", min_width=60) - gr_end_offset = gr.Number(value=0.0, precision=4, step=.01, label="End Offset", min_width=60) - with gr.Column(scale=1, min_width=275): - gr_mode = gr.Dropdown(["both", "cond", "uncond"], value="uncond", label="Mode", show_label=True, min_width=60, elem_classes=['detail-daemon-mode']) - gr_smooth = gr.Checkbox(label="Smooth", value=True, min_width=60, elem_classes=['detail-daemon-smooth']) - gr.Markdown("## [Ⓗ Help](https://github.com/muerrilla/sd-webui-detail-daemon)", elem_classes=['detail-daemon-help']) - gr_amount_slider.release(None, gr_amount_slider, gr_amount, _js="(x) => x") - gr_amount.change(None, gr_amount, gr_amount_slider, _js="(x) => x") - - gr_start_offset_slider.release(None, gr_start_offset_slider, gr_start_offset, _js="(x) => x") - gr_start_offset.change(None, gr_start_offset, gr_start_offset_slider, _js="(x) => x") - - gr_end_offset_slider.release(None, gr_end_offset_slider, gr_end_offset, _js="(x) => x") - gr_end_offset.change(None, gr_end_offset, gr_end_offset_slider, _js="(x) => x") - - vis_args = [gr_enabled, gr_start, gr_end, gr_bias, gr_amount, gr_exponent, gr_start_offset, gr_end_offset, gr_fade, gr_smooth] - for vis_arg in vis_args: - if isinstance(vis_arg, gr.components.Slider): - vis_arg.release(fn=self.visualize, show_progress=False, inputs=vis_args, outputs=[gr_vis]) - else: - vis_arg.change(fn=self.visualize, show_progress=False, inputs=vis_args, outputs=[gr_vis]) - - def extract_infotext(d: dict, key, old_key): + def extract_infotext(d: dict, key, old_key=None): if 'Detail Daemon' in d: return d['Detail Daemon'].get(key) return d.get(old_key) - self.infotext_fields = [ - (gr_enabled, lambda d: True if ('Detail Daemon' in d or 'DD_enabled' in d) else False), - (gr_mode, lambda d: extract_infotext(d, 'mode', 'DD_mode')), - (gr_amount, lambda d: extract_infotext(d, 'amount', 'DD_amount')), - (gr_start, lambda d: extract_infotext(d, 'st', 'DD_start')), - (gr_end, lambda d: extract_infotext(d, 'ed', 'DD_end')), - (gr_bias, lambda d: extract_infotext(d, 'bias', 'DD_bias')), - (gr_exponent, lambda d: extract_infotext(d, 'exp', 'DD_exponent')), - (gr_start_offset, lambda d: extract_infotext(d, 'st_offset', 'DD_start_offset')), - (gr_end_offset, lambda d: extract_infotext(d, 'ed_offset', 'DD_end_offset')), - (gr_fade, lambda d: extract_infotext(d, 'fade', 'DD_fade')), - (gr_smooth, lambda d: extract_infotext(d, 'smooth', 'DD_smooth')), - ] - return [gr_enabled, gr_mode, gr_start, gr_end, gr_bias, gr_amount, gr_exponent, gr_start_offset, gr_end_offset, gr_fade, gr_smooth] + daemon_count = shared.opts.data.get("detail_daemon_count", 6) + all_params = [] + + with InputAccordion(False, label="Detail Daemon", elem_id=self.elem_id('detail-daemon')) as gr_enabled: + all_params.append(gr_enabled) + self.infotext_fields = [(gr_enabled, lambda d: 'Detail Daemon' in d or 'DD_enabled' in d)] + thumbs = [] + with gr.Group(): + with gr.Row(elem_classes=['detail-daemon-thumb-group']): + for i in range(daemon_count): + _, empty = self.visualize(False, 0, 1, 0.5, 0, 0, 0, 0, 0, False, 'both', False) + gr_thumb = gr.Plot(value=empty, elem_classes=['detail-daemon-thumb'], show_label=False) + thumbs.append(gr_thumb) + with gr.Group(elem_classes=['detail-daemon-tab-group']): + for i in range(daemon_count): + with gr.Tab(f'{["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"][i]}', elem_classes=['detail-daemon-tab']): + params_set = [] + with gr.Row(): + gr_active = gr.Checkbox(label="Active", value=False, min_width=60, elem_classes=['detail-daemon-active']) + gr_hires = gr.Checkbox(label="Hires Pass", value=False, min_width=60, elem_classes=['detail-daemon-hires']) + with gr.Row(): + with gr.Column(scale=2, elem_classes=['detail-daemon-params']): + gr_amount_slider = gr.Slider(minimum=-1.00, maximum=1.00, step=.01, value=0.10, label="Detail Amount") + with gr.Row(): + gr_start = gr.Slider(minimum=0.0, maximum=1.0, step=.01, value=0.2, label="Start", min_width=60) + gr_end = gr.Slider(minimum=0.0, maximum=1.0, step=.01, value=0.8, label="End", min_width=60) + with gr.Row(): + gr_start_offset_slider = gr.Slider(minimum=-1.00, maximum=1.00, step=.01, value=0.00, label="Start Offset", min_width=60) + gr_end_offset_slider = gr.Slider(minimum=-1.00, maximum=1.00, step=.01, value=0.00, label="End Offset", min_width=60) + with gr.Row(): + gr_bias = gr.Slider(minimum=0.0, maximum=1.0, step=.01, value=0.5, label="Bias", min_width=60) + gr_exponent = gr.Slider(minimum=0.0, maximum=10.0, step=.05, value=1.0, label="Exponent", min_width=60) + gr_fade = gr.Slider(minimum=0.0, maximum=1.0, step=.05, value=0.0, label="Fade", min_width=60) + with gr.Column(scale=1, min_width=275): + preview, _ = self.visualize(False, 0.2, 0.8, 0.5, 0.1, 1, 0, 0, 0, True, 'both', False) + gr_vis = gr.Plot(value=preview, elem_classes=['detail-daemon-vis'], show_label=False) + gr_smooth = gr.Checkbox(label="Smooth", value=True, min_width=60, elem_classes=['detail-daemon-smooth']) + with gr.Accordion("More Knobs:", elem_classes=['detail-daemon-more-accordion'], open=False): + with gr.Row(): + with gr.Column(scale=2): + with gr.Row(): + # Because the slider max and min are sometimes too limiting: + gr_amount = gr.Number(value=0.10, precision=4, step=.01, label="Amount", min_width=60) + gr_start_offset = gr.Number(value=0.0, precision=4, step=.01, label="Start Offset", min_width=60) + gr_end_offset = gr.Number(value=0.0, precision=4, step=.01, label="End Offset", min_width=60) + gr_mode = gr.Dropdown(["both", "cond", "uncond"], value="both", label="Mode", show_label=True, min_width=60, elem_classes=['detail-daemon-mode']) + + gr_amount_slider.release(None, gr_amount_slider, gr_amount, _js="(x) => x") + gr_amount.change(None, gr_amount, gr_amount_slider, _js="(x) => x") + + gr_start_offset_slider.release(None, gr_start_offset_slider, gr_start_offset, _js="(x) => x") + gr_start_offset.change(None, gr_start_offset, gr_start_offset_slider, _js="(x) => x") + + gr_end_offset_slider.release(None, gr_end_offset_slider, gr_end_offset, _js="(x) => x") + gr_end_offset.change(None, gr_end_offset, gr_end_offset_slider, _js="(x) => x") + + vis_args = [gr_active, gr_start, gr_end, gr_bias, gr_amount, gr_exponent, gr_start_offset, gr_end_offset, gr_fade, gr_smooth, gr_mode, gr_hires] + for vis_arg in vis_args: + if isinstance(vis_arg, gr.components.Slider): + vis_arg.release(fn=self.visualize, show_progress=False, inputs=vis_args, outputs=[gr_vis, thumbs[i]]) + else: + vis_arg.change(fn=self.visualize, show_progress=False, inputs=vis_args, outputs=[gr_vis, thumbs[i]]) + + params_set = [ + gr_active, gr_hires, gr_mode, gr_start, gr_end, gr_bias, gr_amount, + gr_exponent, gr_start_offset, gr_end_offset, gr_fade, gr_smooth + ] + all_params.extend(params_set) + + # First tab should be backward compatible with the older single daemon Detail Daemon, hence the variable names without numbers + # older single daemon DD was backward compatible with yet older DD which had infotext with the DD_stuff format, so that's handled here too + if i == 0 : + self.tab_param_count = len(params_set) + self.infotext_fields.extend([ + (gr_active, lambda d: extract_infotext(d, 'active') or 'Detail Daemon' in d or 'DD_enabled' in d), + (gr_hires, lambda d: extract_infotext(d, 'hr') or False), + (gr_mode, lambda d: extract_infotext(d, 'mode', 'DD_mode')), + (gr_amount, lambda d: extract_infotext(d, 'amount', 'DD_amount')), + (gr_start, lambda d: extract_infotext(d, 'st', 'DD_start')), + (gr_end, lambda d: extract_infotext(d, 'ed', 'DD_end')), + (gr_bias, lambda d: extract_infotext(d, 'bias', 'DD_bias')), + (gr_exponent, lambda d: extract_infotext(d, 'exp', 'DD_exponent')), + (gr_start_offset, lambda d: extract_infotext(d, 'st_offset', 'DD_start_offset')), + (gr_end_offset, lambda d: extract_infotext(d, 'ed_offset', 'DD_end_offset')), + (gr_fade, lambda d: extract_infotext(d, 'fade', 'DD_fade')), + (gr_smooth, lambda d: extract_infotext(d, 'smooth', 'DD_smooth')), + ]) + else: + tab_tag = i + 1 + self.infotext_fields.extend([ + (gr_active, lambda d, key=f'active{tab_tag}': extract_infotext(d, key) or False), + (gr_hires, lambda d, key=f'hr{tab_tag}': extract_infotext(d, key)), + (gr_mode, lambda d, key=f'mode{tab_tag}': extract_infotext(d, key)), + (gr_amount, lambda d, key=f'amount{tab_tag}': extract_infotext(d, key)), + (gr_start, lambda d, key=f'st{tab_tag}': extract_infotext(d, key)), + (gr_end, lambda d, key=f'ed{tab_tag}': extract_infotext(d, key)), + (gr_bias, lambda d, key=f'bias{tab_tag}': extract_infotext(d, key)), + (gr_exponent, lambda d, key=f'exp{tab_tag}': extract_infotext(d, key)), + (gr_start_offset, lambda d, key=f'st_offset{tab_tag}': extract_infotext(d, key)), + (gr_end_offset, lambda d, key=f'ed_offset{tab_tag}': extract_infotext(d, key)), + (gr_fade, lambda d, key=f'fade{tab_tag}': extract_infotext(d, key)), + (gr_smooth, lambda d, key=f'smooth{tab_tag}': extract_infotext(d, key)), + ]) + return all_params - def process(self, p, enabled, mode, start, end, bias, amount, exponent, start_offset, end_offset, fade, smooth): - - enabled = getattr(p, "DD_enabled", enabled) - mode = getattr(p, "DD_mode", mode) - amount = getattr(p, "DD_amount", amount) - start = getattr(p, "DD_start", start) - end = getattr(p, "DD_end", end) - bias = getattr(p, "DD_bias", bias) - exponent = getattr(p, "DD_exponent", exponent) - start_offset = getattr(p, "DD_start_offset", start_offset) - end_offset = getattr(p, "DD_end_offset", end_offset) - fade = getattr(p, "DD_fade", fade) - smooth = getattr(p, "DD_smooth", smooth) - - if enabled: - if p.sampler_name in ["DPM adaptive", "HeunPP2"]: - tqdm.write(f'\033[33mWARNING:\033[0m Detail Daemon does not work with {p.sampler_name}') - return - - self.schedule_params = { - "start": start, - "end": end, - "bias": bias, - "amount": amount, - "exponent": exponent, - "start_offset": start_offset, - "end_offset": end_offset, - "fade": fade, - "smooth": smooth - } - self.mode = mode - self.cfg_scale = p.cfg_scale - self.batch_size = p.batch_size - on_cfg_denoiser(self.denoiser_callback) - self.callback_added = True - p.extra_generation_params['Detail Daemon'] = f'mode:{mode},amount:{amount},st:{start},ed:{end},bias:{bias},exp:{exponent},st_offset:{start_offset},ed_offset:{end_offset},fade:{fade},smooth:{1 if smooth else 0}' - tqdm.write('\033[32mINFO:\033[0m Detail Daemon is enabled') - else: + def process(self, p, enabled, *all_daemon_args): + if not enabled: if hasattr(self, 'callback_added'): remove_callbacks_for_function(self.denoiser_callback) delattr(self, 'callback_added') - self.schedule = None - # tqdm.write('\033[90mINFO: Detail Daemon callback removed\033[0m') + return + + if p.sampler_name in ["DPM adaptive", "HeunPP2"]: + tqdm.write(f'\033[31mDetail Daemon:\033[0m Selected sampler ({p.sampler_name}) is not supported.') + return + + self.daemon_data = [] + extra_gen_texts = [] + num_daemons = len(all_daemon_args) // self.tab_param_count + + for i in range(num_daemons): + start_idx = i * self.tab_param_count + end_idx = start_idx + self.tab_param_count + daemon_args = all_daemon_args[start_idx:end_idx] + + active, hires, mode, start, end, bias, amount, exponent, start_offset, end_offset, fade, smooth = daemon_args + + # TODO? XYZ support for other channels + if (i == 0) : + mode = getattr(p, "DD_mode", mode) + amount = getattr(p, "DD_amount", amount) + start = getattr(p, "DD_start", start) + end = getattr(p, "DD_end", end) + bias = getattr(p, "DD_bias", bias) + exponent = getattr(p, "DD_exponent", exponent) + start_offset = getattr(p, "DD_start_offset", start_offset) + end_offset = getattr(p, "DD_end_offset", end_offset) + fade = getattr(p, "DD_fade", fade) + smooth = getattr(p, "DD_smooth", smooth) + + if active: + daemon_schedule_params = { + "start": start, + "end": end, + "bias": bias, + "amount": amount, + "exponent": exponent, + "start_offset": start_offset, + "end_offset": end_offset, + "fade": fade, + "smooth": smooth + } + + self.daemon_data.append({ + 'name': f'Daemon {i+1}', + 'mode': mode, + 'schedule': None, + 'schedule_params': daemon_schedule_params, + 'hires': hires, + 'multiplier': .1 # Add slider for this? + }) + + text = ",".join([ + str(int(active)), str(int(hires)), mode, f"{amount}", f"{start}", f"{end}", f"{bias}", + f"{exponent}", f"{start_offset}", f"{end_offset}", f"{fade:}", str(int(smooth)) + ]) + extra_gen_texts.append(f"D{i+1}:{text}") + + if extra_gen_texts: + p.extra_generation_params['Detail Daemon'] = ";".join(extra_gen_texts) + + if not hasattr(self, 'callback_added'): + on_cfg_denoiser(self.denoiser_callback) + self.callback_added = True + self.cfg_scale = p.cfg_scale + self.batch_size = p.batch_size + self.is_hires_pass = False def before_process_batch(self, p, *args, **kwargs): - self.is_hires = False + self.is_hires_pass = False + + def before_hr(self, p, *args): + self.is_hires_pass = True def postprocess(self, p, processed, *args): if hasattr(self, 'callback_added'): remove_callbacks_for_function(self.denoiser_callback) - delattr(self, 'callback_added') - self.schedule = None - # tqdm.write('\033[90mINFO: Detail Daemon callback removed\033[0m') - - def before_hr(self, p, *args): - self.is_hires = True - enabled = args[0] - if enabled: - tqdm.write(f'\033[33mINFO:\033[0m Detail Daemon does not work during Hires Fix') + delattr(self, 'callback_added') def denoiser_callback(self, params): - if self.is_hires: - return - step = max(params.sampling_step, params.denoiser.step) - total_steps = max(params.total_sampling_steps, params.denoiser.total_steps) - corrected_step_count = total_steps - max(total_steps // params.denoiser.steps - 1, 0) - if self.schedule is None: - self.schedule = self.make_schedule(corrected_step_count, **self.schedule_params) + for daemon in self.daemon_data: + if daemon['hires'] != self.is_hires_pass: + continue - idx = min(step, corrected_step_count - 1) - multiplier = self.schedule[idx] * .1 - mode = self.mode - if params.sigma.size(0) == 1 and mode != "both": - mode = "both" - if idx == 0: - tqdm.write(f'\033[33mWARNING:\033[0m Forge does not support `cond` and `uncond` modes, using `both` instead') - if mode == "cond": - for i in range(self.batch_size): - params.sigma[i] *= 1 - multiplier - elif mode == "uncond": - for i in range(self.batch_size): - params.sigma[self.batch_size + i] *= 1 + multiplier - else: - params.sigma *= 1 - multiplier * self.cfg_scale + name = daemon['name'] + mode = daemon['mode'] + step = max(params.sampling_step, params.denoiser.step) + steps = max(params.total_sampling_steps, params.denoiser.total_steps) + actual_steps = steps - max(steps // params.denoiser.steps - 1, 0) + idx = min(step, actual_steps - 1) + + if daemon['schedule'] is None: + daemon['schedule'] = self.make_schedule(actual_steps, **daemon['schedule_params']) + + schedule = daemon['schedule'] + multiplier = schedule[idx] * daemon['multiplier'] + + if is_forge: + if idx == 0 and mode != "both": + tqdm.write(f'\033[33mDetail Daemon:\033[0m Forge does not support `cond` and `uncond` modes, using `both` instead') + mode = "both" + + if mode == "cond": + for i in range(self.batch_size): + params.sigma[i] *= 1 - multiplier + elif mode == "uncond": + for i in range(self.batch_size): + params.sigma[self.batch_size + i] *= 1 + multiplier + else: + params.sigma *= 1 - multiplier * self.cfg_scale + + if shared.opts.data.get("detail_daemon_verbose", False): + tqdm.write(f'\033[32mDetail Daemon:\033[0m {name} | sigma: {params.sigma} | step: {idx}/{actual_steps - 1} | multiplier: {multiplier:.4f}') def make_schedule(self, steps, start, end, bias, amount, exponent, start_offset, end_offset, fade, smooth): start = min(start, end) @@ -221,7 +337,7 @@ class Script(scripts.Script): return multipliers - def visualize(self, enabled, start, end, bias, amount, exponent, start_offset, end_offset, fade, smooth): + def visualize(self, enabled, start, end, bias, amount, exponent, start_offset, end_offset, fade, smooth, mode, hires): try: steps = 50 values = self.make_schedule(steps, start, end, bias, amount, exponent, start_offset, end_offset, fade, smooth) @@ -232,6 +348,7 @@ class Script(scripts.Script): mid = start + bias * (end - start) opacity = .1 + (1 - fade) * 0.7 plot_color = (0.5, 0.5, 0.5, opacity) if not enabled else ((1 - peak)**2, 1, 0, opacity) if mean >= 0 else (1, (1 - peak)**2, 0, opacity) + plt.rcParams.update({ "text.color": plot_color, "axes.labelcolor": plot_color, @@ -241,24 +358,48 @@ class Script(scripts.Script): "ytick.labelsize": 6, "ytick.labelcolor": plot_color, "ytick.color": plot_color, - }) - fig, ax = plt.subplots(figsize=(2.15, 2.00), layout="constrained") - ax.plot(range(steps), values, color=plot_color) - ax.axhline(y=0, color=plot_color, linestyle='dotted') - ax.axvline(x=mid * (steps - 1), color=plot_color, linestyle='dotted') - ax.tick_params(right=False, color=plot_color) - ax.set_xticks([i * (steps - 1) / 10 for i in range(10)][1:]) - ax.set_xticklabels([]) - ax.set_ylim([-1, 1]) - ax.set_xlim([0, steps-1]) - plt.close() - self.last_vis = fig - return fig - except Exception: - if self.last_vis is not None: - return self.last_vis - return + }) + fig_main, ax_main = plt.subplots(figsize=(2.15, 2.00), layout="constrained") + ax_main.plot(range(steps), values, color=plot_color, linewidth=1.5, linestyle="dashed" if hires else "solid") + ax_main.axhline(y=0, color=plot_color, linestyle='dotted') + ax_main.axvline(x=mid * (steps - 1), color=plot_color, linestyle='dotted') + ax_main.tick_params(right=False, color=plot_color) + ax_main.set_xticks([i * (steps - 1) / 10 for i in range(10)][1:]) + ax_main.set_xticklabels([]) + ax_main.set_ylim([-1, 1]) + ax_main.set_xlim([0, steps - 1]) + plt.close(fig_main) + + plot_color = (0.5, 0.5, 0.5, .1) if not enabled else (0.75, 0.75, 0.75, opacity) + plt.rcParams.update({ + "text.color": plot_color, + "axes.labelcolor": plot_color, + "axes.edgecolor": plot_color, + }) + + fig_thumb, ax_thumb = plt.subplots(figsize=(0.85, 0.85), layout="constrained") + ax_thumb.plot(range(steps), values, color=plot_color, linewidth=1.5, linestyle="dashed" if hires else "solid") + ax_thumb.set_xticks([]) + ax_thumb.set_yticks([]) + ax_thumb.set_ylim([-1, 1]) + ax_thumb.set_xlim([0, steps - 1]) + if (mode != "both"): + ax_thumb.text( + 0.98, 0.96, mode.upper(), + transform=ax_thumb.transAxes, + fontsize=8, fontweight='bold', color=plot_color, + ha='right', va='top' + ) + plt.close(fig_thumb) + + self.last_vis = fig_main + self.last_thumb = fig_thumb + return [fig_main, fig_thumb] + except Exception: + if self.last_vis is not None and self.last_thumb is not None: + return [self.last_vis, self.last_thumb] + return def xyz_support(): for scriptDataTuple in scripts.scripts_data: @@ -314,7 +455,12 @@ def xyz_support(): '[Detail Daemon] Fade', float, xy_grid.apply_field('DD_fade') - ) + ) + smooth = xy_grid.AxisOption( + '[Detail Daemon] Smooth', + bool, + xy_grid.apply_field('DD_smooth') + ) xy_grid.axis_options.extend([ mode, amount, @@ -325,10 +471,11 @@ def xyz_support(): start_offset, end_offset, fade, + smooth, ]) try: xyz_support() except Exception as e: - print(f'Error trying to add XYZ plot options for Detail Daemon', e) + tqdm.write(f'f"\033[31mDetail Daemon:\033[0m Error trying to add XYZ plot options for Detail Daemon: {e}')