├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── scripts └── detail_daemon.py └── style.css /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sahand Ahmadian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Detail Daemon 2 | This is an extension for [Stable Diffusion Web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui), which allows users to adjust the amount of detail/smoothness in an image, during the sampling steps. 3 | 4 | It uses no LORAs, ControlNets, etc., and as a result its performance is not biased towards any certain style and it introduces no new stylistic or semantic features of its own into the generation. This also means that it can work with any model and on any type of image. 5 | 6 | *Model: SSD-1B*
7 | ![a close up portrait of a cyberpunk knight-1Lv-0](https://github.com/muerrilla/sd-webui-detail-daemon/assets/48160881/561c33d9-9a5d-4cfc-bee8-de9126b280c1) 8 | *Left: Less detail, Middle: Original, Right: More detail*
9 | 10 | *Model: SD 1.5 (finetuned)*
11 | ![face of a cute cat love heart symbol-Zn6-0](https://github.com/muerrilla/sd-webui-detail-daemon/assets/48160881/9fbfb39f-81fb-4951-8f32-20eab410020a) 12 | *Left: Less detail, Middle: Original, Right: More detail*
13 | 14 | 15 | ## How It Works 16 | Detail Daemon works by manipulating the original noise levels at every sampling step, according to a customizable schedule. 17 | 18 | ### In Theory 19 | The noise levels (sigmas, i.e. the standard deviation of the noise) tell the model how much noise it should expect, and try to remove, at each denoising step. A higher sigma value at a certain denoising step tells the model to denoise more aggressively at that step and vice versa. 20 | 21 | With a common sigmas schedule, the sigmas start at very high values at the beginning of the denoising process, then quickly fall to low values in the middle, and to very low values towards the end of the process. This curve (along with the timesteps schedule, but that's a story for another day) is what makes it so that larger features (low frequencies) of the image are defined at the earlier steps, and towards the end of the process you can only see minor changes in the smaller features (high frequencies). We'll get back to this later. 22 | 23 | Now, if we pass the model a sigmas schedule with values lower than the original, at each step the model will denoise less, resulting a noisier output latent at that step. But then in the steps after that, the model does its best to make sense of this extra noise and turn it into image features. So in theory, *when done in modesty*, this would result in a more detailed image. If you push it too hard, the model won't be able to handle the extra noise added at each step and the end result will devolve into pure noise. So modesty is key. 24 | 25 | ### But in Practice 26 | Modesty only gets you so far! Also, wtf are those? As the examples below show, you can't really add that much detail to the image before it either breaks down, and/or becomes a totally different thing. 27 | 28 | *SD 1.5*
29 | ![Modesty](https://github.com/muerrilla/sd-webui-detail-daemon/assets/48160881/2f011a28-0948-48f8-b171-350add6fdd67) 30 | Original sigmas (left) multiplied by .9, .85, .8
31 | 32 | *SDXL*
33 | ![1](https://github.com/muerrilla/sd-webui-detail-daemon/assets/48160881/eff2356e-a6dd-4a4e-9c7e-861dec7713eb) 34 | Original sigmas (left) multiplied by .95, .9, .85, .875, .8
35 | 36 | That's because: 37 | 1. We're constantly adding noise and not giving the model enough time to deal with it 38 | 2. We are manipulating the early steps where the low frequency features of the image (color, composition, etc.) are defined 39 | 40 | ### Enter the Schedule 41 | What we usually mean by "detail" falls within the mid to high frequency range, which correspond to the middle to late steps in the sampling process. So if we skip the early steps to leave the main features of the image intact, and the late steps to give the model some time to turn the extra noise into useful detail, we'll have something like this: 42 | 43 | ![3](https://github.com/muerrilla/sd-webui-detail-daemon/assets/48160881/cd47e882-8b56-4321-8c47-c0d689562780) 44 | 45 | Then we could make our schedule a bit fancier and have it target specific steps corresponding to different sized details: 46 | 47 | ![4](https://github.com/muerrilla/sd-webui-detail-daemon/assets/48160881/ea5027d2-3359-4733-afb4-5ae4a1218f38) 48 | 49 | Which steps correspond to which exact frequency range depends on the model you're using, the sampler, your prompt (specially if you're using Prompt Editing and stuff), and probably a bunch of other things. There are also fancier things you can (and should) do with the schedule, like pushing the sigmas too low for some heavy extra noise and then too high to clean up the excess and leave some nice details. So you need to do some tweaking to figure out the best schedule for each image you generate, or at least the ones that need their level of detail adjusted. But ideally you should be spending countless hours of your life sculpting the perfect detail adjustment schedule for every image, cuz that's why we're here. 50 | 51 | I'll soon provide specific examples addressing different scenarios and some of the techniques I've come up with. (note to self: move these to the wiki page) 52 | 53 | ## Installation 54 | Open SD WebUI > Go to Extensions tab > Go to Available tab > Click Load from: > Find Detail Daemon > Click Install 55 | 56 | Or Go to Install from URL tab > Paste this repo's URL into the first field > Click Install 57 | 58 | Or go to your WebUI folder and manually clone this repo into your extensions folder: 59 | 60 | `git clone "https://github.com/muerrilla/sd-webui-detail-daemon" extensions/sd-webui-detail-daemon` 61 | 62 | ## Getting Started 63 | After installation you can find the extension in your txt2img and img2img tabs. 64 | ![2024-07-08 01_43_21-011366](https://github.com/muerrilla/sd-webui-detail-daemon/assets/48160881/045574cb-465c-4991-83c4-d02f803a330b) 65 | ### Sliders: 66 | The sliders (and that one checkbox) set the amount of adjustment (positive values → add detail, negative values → remove detail) and the sampling steps during which it is applied (i.e. the schedule). So the X axis of the graph is your sampling steps, normalized to the (0,1) range, and the Y axis is the amount of adjustment. The rest is pretty self-explanatory I think. Just drag things and look at the graph for changes. 67 | ### Numbers: 68 | The three number inputs at the buttom are provided because sometimes the slider max/mins are too limiting. 69 | ### Modes: 70 | The `cond` and `uncond` modes affect only their respective latents, while `both` affects both (duh!). The `cond` and `uncond` modes are less intense and also allow changes to be applied at earlier steps without diverging too far from the original generation, since the other latent stays intact. 71 | 72 | There's also a minor twist: in the `both` mode if `detail amount` is positive both cond and uncond latents become more detailed. So the more detailed cond latent will try to push the generation towards more detail, while the more detailed uncond latent will try to push towards less detail. This causes more new features/artifacts to pop into the image in this mode. 73 | 74 | ### Tips: 75 | I'll write up some proper docs on how best to set the parameters, as soon as possible. For now you gotta play around with the sliders and figure out how the shape of the schedule affects the image. I suggest you set your live preview update period to every frame, or every other frame, so you could see clearly what's going on at every step of the sampling process and how Detail Daemon affects it, till you get a good grasp of how this thing works. 76 | 77 | ## Notes: 78 | - Doesn't support Compositional Diffusion (i.e. the AND syntax) properly. Specially if you have a batch size > 1 or negative weights in your prompts, and the mode is set to `cond` or `uncond`. 79 | - It's probably impossible to use or very hard to control with few-step models (Turbo, Lightning, etc.). Edit: It's managable. 80 | - It works with Forge (`cond` and `uncond` modes are not supported). 81 | - It's not the same as AlignYourSteps, FreeU, etc. 82 | - It is similar (in what it sets out to do, not in how it does it) to the [ReSharpen Extension](https://github.com/Haoming02/sd-webui-resharpen) by Haoming. 83 | -------------------------------------------------------------------------------- /scripts/detail_daemon.py: -------------------------------------------------------------------------------- 1 | import os 2 | import gradio as gr 3 | import numpy as np 4 | from tqdm import tqdm 5 | 6 | import matplotlib 7 | matplotlib.use('Agg') 8 | import matplotlib.pyplot as plt 9 | 10 | import modules.scripts as scripts 11 | from modules.script_callbacks import on_cfg_denoiser, remove_callbacks_for_function, on_infotext_pasted 12 | from modules.ui_components import InputAccordion 13 | 14 | 15 | def parse_infotext(infotext, params): 16 | try: 17 | d = {} 18 | for s in params['Detail Daemon'].split(','): 19 | k, _, v = s.partition(':') 20 | d[k.strip()] = v.strip() 21 | params['Detail Daemon'] = d 22 | except Exception: 23 | pass 24 | 25 | 26 | on_infotext_pasted(parse_infotext) 27 | 28 | 29 | class Script(scripts.Script): 30 | 31 | def __init__(self): 32 | super().__init__() 33 | self.schedule_params: dict[str, float] = None 34 | self.schedule = None 35 | 36 | def title(self): 37 | return "Detail Daemon" 38 | 39 | def show(self, is_img2img): 40 | return scripts.AlwaysVisible 41 | 42 | def ui(self, is_img2img): 43 | with InputAccordion(False, label="Detail Daemon", elem_id=self.elem_id('detail-daemon')) as gr_enabled: 44 | with gr.Row(): 45 | with gr.Column(scale=2): 46 | gr_amount_slider = gr.Slider(minimum=-1.00, maximum=1.00, step=.01, value=0.10, label="Detail Amount") 47 | gr_start = gr.Slider(minimum=0.0, maximum=1.0, step=.01, value=0.2, label="Start") 48 | gr_end = gr.Slider(minimum=0.0, maximum=1.0, step=.01, value=0.8, label="End") 49 | gr_bias = gr.Slider(minimum=0.0, maximum=1.0, step=.01, value=0.5, label="Bias") 50 | with gr.Column(scale=1, min_width=275): 51 | preview = self.visualize(False, 0.2, 0.8, 0.5, 0.1, 1, 0, 0, 0, True) 52 | gr_vis = gr.Plot(value=preview, elem_classes=['detail-daemon-vis'], show_label=False) 53 | with gr.Accordion("More Knobs:", elem_classes=['detail-daemon-more-accordion'], open=False): 54 | with gr.Row(): 55 | with gr.Column(scale=2): 56 | with gr.Row(): 57 | gr_start_offset_slider = gr.Slider(minimum=-1.00, maximum=1.00, step=.01, value=0.00, label="Start Offset", min_width=60) 58 | gr_end_offset_slider = gr.Slider(minimum=-1.00, maximum=1.00, step=.01, value=0.00, label="End Offset", min_width=60) 59 | with gr.Row(): 60 | gr_exponent = gr.Slider(minimum=0.0, maximum=10.0, step=.05, value=1.0, label="Exponent", min_width=60) 61 | gr_fade = gr.Slider(minimum=0.0, maximum=1.0, step=.05, value=0.0, label="Fade", min_width=60) 62 | # Because the slider max and min are sometimes too limiting: 63 | with gr.Row(): 64 | gr_amount = gr.Number(value=0.10, precision=4, step=.01, label="Amount", min_width=60) 65 | gr_start_offset = gr.Number(value=0.0, precision=4, step=.01, label="Start Offset", min_width=60) 66 | gr_end_offset = gr.Number(value=0.0, precision=4, step=.01, label="End Offset", min_width=60) 67 | with gr.Column(scale=1, min_width=275): 68 | gr_mode = gr.Dropdown(["both", "cond", "uncond"], value="uncond", label="Mode", show_label=True, min_width=60, elem_classes=['detail-daemon-mode']) 69 | gr_smooth = gr.Checkbox(label="Smooth", value=True, min_width=60, elem_classes=['detail-daemon-smooth']) 70 | gr.Markdown("## [Ⓗ Help](https://github.com/muerrilla/sd-webui-detail-daemon)", elem_classes=['detail-daemon-help']) 71 | 72 | gr_amount_slider.release(None, gr_amount_slider, gr_amount, _js="(x) => x") 73 | gr_amount.change(None, gr_amount, gr_amount_slider, _js="(x) => x") 74 | 75 | gr_start_offset_slider.release(None, gr_start_offset_slider, gr_start_offset, _js="(x) => x") 76 | gr_start_offset.change(None, gr_start_offset, gr_start_offset_slider, _js="(x) => x") 77 | 78 | gr_end_offset_slider.release(None, gr_end_offset_slider, gr_end_offset, _js="(x) => x") 79 | gr_end_offset.change(None, gr_end_offset, gr_end_offset_slider, _js="(x) => x") 80 | 81 | vis_args = [gr_enabled, gr_start, gr_end, gr_bias, gr_amount, gr_exponent, gr_start_offset, gr_end_offset, gr_fade, gr_smooth] 82 | for vis_arg in vis_args: 83 | if isinstance(vis_arg, gr.components.Slider): 84 | vis_arg.release(fn=self.visualize, show_progress=False, inputs=vis_args, outputs=[gr_vis]) 85 | else: 86 | vis_arg.change(fn=self.visualize, show_progress=False, inputs=vis_args, outputs=[gr_vis]) 87 | 88 | def extract_infotext(d: dict, key, old_key): 89 | if 'Detail Daemon' in d: 90 | return d['Detail Daemon'].get(key) 91 | return d.get(old_key) 92 | 93 | self.infotext_fields = [ 94 | (gr_enabled, lambda d: True if ('Detail Daemon' in d or 'DD_enabled' in d) else False), 95 | (gr_mode, lambda d: extract_infotext(d, 'mode', 'DD_mode')), 96 | (gr_amount, lambda d: extract_infotext(d, 'amount', 'DD_amount')), 97 | (gr_start, lambda d: extract_infotext(d, 'st', 'DD_start')), 98 | (gr_end, lambda d: extract_infotext(d, 'ed', 'DD_end')), 99 | (gr_bias, lambda d: extract_infotext(d, 'bias', 'DD_bias')), 100 | (gr_exponent, lambda d: extract_infotext(d, 'exp', 'DD_exponent')), 101 | (gr_start_offset, lambda d: extract_infotext(d, 'st_offset', 'DD_start_offset')), 102 | (gr_end_offset, lambda d: extract_infotext(d, 'ed_offset', 'DD_end_offset')), 103 | (gr_fade, lambda d: extract_infotext(d, 'fade', 'DD_fade')), 104 | (gr_smooth, lambda d: extract_infotext(d, 'smooth', 'DD_smooth')), 105 | ] 106 | 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] 107 | 108 | def process(self, p, enabled, mode, start, end, bias, amount, exponent, start_offset, end_offset, fade, smooth): 109 | 110 | enabled = getattr(p, "DD_enabled", enabled) 111 | mode = getattr(p, "DD_mode", mode) 112 | amount = getattr(p, "DD_amount", amount) 113 | start = getattr(p, "DD_start", start) 114 | end = getattr(p, "DD_end", end) 115 | bias = getattr(p, "DD_bias", bias) 116 | exponent = getattr(p, "DD_exponent", exponent) 117 | start_offset = getattr(p, "DD_start_offset", start_offset) 118 | end_offset = getattr(p, "DD_end_offset", end_offset) 119 | fade = getattr(p, "DD_fade", fade) 120 | smooth = getattr(p, "DD_smooth", smooth) 121 | 122 | if enabled: 123 | if p.sampler_name in ["DPM adaptive", "HeunPP2"]: 124 | tqdm.write(f'\033[33mWARNING:\033[0m Detail Daemon does not work with {p.sampler_name}') 125 | return 126 | 127 | self.schedule_params = { 128 | "start": start, 129 | "end": end, 130 | "bias": bias, 131 | "amount": amount, 132 | "exponent": exponent, 133 | "start_offset": start_offset, 134 | "end_offset": end_offset, 135 | "fade": fade, 136 | "smooth": smooth 137 | } 138 | self.mode = mode 139 | self.cfg_scale = p.cfg_scale 140 | self.batch_size = p.batch_size 141 | on_cfg_denoiser(self.denoiser_callback) 142 | self.callback_added = True 143 | 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}' 144 | tqdm.write('\033[32mINFO:\033[0m Detail Daemon is enabled') 145 | else: 146 | if hasattr(self, 'callback_added'): 147 | remove_callbacks_for_function(self.denoiser_callback) 148 | delattr(self, 'callback_added') 149 | self.schedule = None 150 | # tqdm.write('\033[90mINFO: Detail Daemon callback removed\033[0m') 151 | 152 | def before_process_batch(self, p, *args, **kwargs): 153 | self.is_hires = False 154 | 155 | def postprocess(self, p, processed, *args): 156 | if hasattr(self, 'callback_added'): 157 | remove_callbacks_for_function(self.denoiser_callback) 158 | delattr(self, 'callback_added') 159 | self.schedule = None 160 | # tqdm.write('\033[90mINFO: Detail Daemon callback removed\033[0m') 161 | 162 | def before_hr(self, p, *args): 163 | self.is_hires = True 164 | enabled = args[0] 165 | if enabled: 166 | tqdm.write(f'\033[33mINFO:\033[0m Detail Daemon does not work during Hires Fix') 167 | 168 | def denoiser_callback(self, params): 169 | if self.is_hires: 170 | return 171 | step = max(params.sampling_step, params.denoiser.step) 172 | total_steps = max(params.total_sampling_steps, params.denoiser.total_steps) 173 | corrected_step_count = total_steps - max(total_steps // params.denoiser.steps - 1, 0) 174 | if self.schedule is None: 175 | self.schedule = self.make_schedule(corrected_step_count, **self.schedule_params) 176 | 177 | idx = min(step, corrected_step_count - 1) 178 | multiplier = self.schedule[idx] * .1 179 | mode = self.mode 180 | if params.sigma.size(0) == 1 and mode != "both": 181 | mode = "both" 182 | if idx == 0: 183 | tqdm.write(f'\033[33mWARNING:\033[0m Forge does not support `cond` and `uncond` modes, using `both` instead') 184 | if mode == "cond": 185 | for i in range(self.batch_size): 186 | params.sigma[i] *= 1 - multiplier 187 | elif mode == "uncond": 188 | for i in range(self.batch_size): 189 | params.sigma[self.batch_size + i] *= 1 + multiplier 190 | else: 191 | params.sigma *= 1 - multiplier * self.cfg_scale 192 | 193 | def make_schedule(self, steps, start, end, bias, amount, exponent, start_offset, end_offset, fade, smooth): 194 | start = min(start, end) 195 | mid = start + bias * (end - start) 196 | multipliers = np.zeros(steps) 197 | 198 | start_idx, mid_idx, end_idx = [int(round(x * (steps - 1))) for x in [start, mid, end]] 199 | 200 | start_values = np.linspace(0, 1, mid_idx - start_idx + 1) 201 | if smooth: 202 | start_values = 0.5 * (1 - np.cos(start_values * np.pi)) 203 | start_values = start_values ** exponent 204 | if start_values.any(): 205 | start_values *= (amount - start_offset) 206 | start_values += start_offset 207 | 208 | end_values = np.linspace(1, 0, end_idx - mid_idx + 1) 209 | if smooth: 210 | end_values = 0.5 * (1 - np.cos(end_values * np.pi)) 211 | end_values = end_values ** exponent 212 | if end_values.any(): 213 | end_values *= (amount - end_offset) 214 | end_values += end_offset 215 | 216 | multipliers[start_idx:mid_idx+1] = start_values 217 | multipliers[mid_idx:end_idx+1] = end_values 218 | multipliers[:start_idx] = start_offset 219 | multipliers[end_idx+1:] = end_offset 220 | multipliers *= 1 - fade 221 | 222 | return multipliers 223 | 224 | def visualize(self, enabled, start, end, bias, amount, exponent, start_offset, end_offset, fade, smooth): 225 | try: 226 | steps = 50 227 | values = self.make_schedule(steps, start, end, bias, amount, exponent, start_offset, end_offset, fade, smooth) 228 | mean = sum(values)/steps 229 | peak = np.clip(max(abs(values)), -1, 1) 230 | if start > end: 231 | start = end 232 | mid = start + bias * (end - start) 233 | opacity = .1 + (1 - fade) * 0.7 234 | 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) 235 | plt.rcParams.update({ 236 | "text.color": plot_color, 237 | "axes.labelcolor": plot_color, 238 | "axes.edgecolor": plot_color, 239 | "figure.facecolor": (0.0, 0.0, 0.0, 0.0), 240 | "axes.facecolor": (0.0, 0.0, 0.0, 0.0), 241 | "ytick.labelsize": 6, 242 | "ytick.labelcolor": plot_color, 243 | "ytick.color": plot_color, 244 | }) 245 | fig, ax = plt.subplots(figsize=(2.15, 2.00), layout="constrained") 246 | ax.plot(range(steps), values, color=plot_color) 247 | ax.axhline(y=0, color=plot_color, linestyle='dotted') 248 | ax.axvline(x=mid * (steps - 1), color=plot_color, linestyle='dotted') 249 | ax.tick_params(right=False, color=plot_color) 250 | ax.set_xticks([i * (steps - 1) / 10 for i in range(10)][1:]) 251 | ax.set_xticklabels([]) 252 | ax.set_ylim([-1, 1]) 253 | ax.set_xlim([0, steps-1]) 254 | plt.close() 255 | self.last_vis = fig 256 | return fig 257 | except Exception: 258 | if self.last_vis is not None: 259 | return self.last_vis 260 | return 261 | 262 | 263 | def xyz_support(): 264 | for scriptDataTuple in scripts.scripts_data: 265 | if os.path.basename(scriptDataTuple.path) == 'xyz_grid.py': 266 | xy_grid = scriptDataTuple.module 267 | 268 | def confirm_mode(p, xs): 269 | for x in xs: 270 | if x not in ['both', 'cond', 'uncond']: 271 | raise RuntimeError(f'Invalid Detail Daemon Mode: {x}') 272 | mode = xy_grid.AxisOption( 273 | '[Detail Daemon] Mode', 274 | str, 275 | xy_grid.apply_field('DD_mode'), 276 | confirm=confirm_mode 277 | ) 278 | amount = xy_grid.AxisOption( 279 | '[Detail Daemon] Amount', 280 | float, 281 | xy_grid.apply_field('DD_amount') 282 | ) 283 | start = xy_grid.AxisOption( 284 | '[Detail Daemon] Start', 285 | float, 286 | xy_grid.apply_field('DD_start') 287 | ) 288 | end = xy_grid.AxisOption( 289 | '[Detail Daemon] End', 290 | float, 291 | xy_grid.apply_field('DD_end') 292 | ) 293 | bias = xy_grid.AxisOption( 294 | '[Detail Daemon] Bias', 295 | float, 296 | xy_grid.apply_field('DD_bias') 297 | ) 298 | exponent = xy_grid.AxisOption( 299 | '[Detail Daemon] Exponent', 300 | float, 301 | xy_grid.apply_field('DD_exponent') 302 | ) 303 | start_offset = xy_grid.AxisOption( 304 | '[Detail Daemon] Start Offset', 305 | float, 306 | xy_grid.apply_field('DD_start_offset') 307 | ) 308 | end_offset = xy_grid.AxisOption( 309 | '[Detail Daemon] End Offset', 310 | float, 311 | xy_grid.apply_field('DD_end_offset') 312 | ) 313 | fade = xy_grid.AxisOption( 314 | '[Detail Daemon] Fade', 315 | float, 316 | xy_grid.apply_field('DD_fade') 317 | ) 318 | xy_grid.axis_options.extend([ 319 | mode, 320 | amount, 321 | start, 322 | end, 323 | bias, 324 | exponent, 325 | start_offset, 326 | end_offset, 327 | fade, 328 | ]) 329 | 330 | 331 | try: 332 | xyz_support() 333 | except Exception as e: 334 | print(f'Error trying to add XYZ plot options for Detail Daemon', e) 335 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | .detail-daemon-more-accordion { 2 | margin-top: 1em !important; 3 | } 4 | 5 | .detail-daemon-mode { 6 | margin-left: 3em !important; 7 | width: 75% !important; 8 | } 9 | 10 | .detail-daemon-smooth { 11 | margin-left: 3em !important; 12 | } 13 | 14 | .detail-daemon-vis { 15 | margin: auto !important; 16 | } 17 | 18 | .detail-daemon-help { 19 | margin: auto 1.5em !important; 20 | } 21 | --------------------------------------------------------------------------------