open-watercolor-sim 0.1.1__py3-none-any.whl

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.
@@ -0,0 +1,305 @@
1
+ """
2
+ Open Watercolor Sim.
3
+
4
+ Copyright (c) 2026 Shuoqi Chen
5
+ SPDX-License-Identifier: Apache-2.0
6
+ """
7
+ import taichi as ti
8
+ import numpy as np
9
+ import time
10
+ import PIL.Image
11
+ import os
12
+ import multiprocessing as mp
13
+ from .brush import WatercolorEngine, SimParams
14
+
15
+ def hsv_to_rgb(h, s, v):
16
+ h = h % 1.0
17
+ i = int(h * 6.0)
18
+ f = (h * 6.0) - i
19
+ p = v * (1.0 - s)
20
+ q = v * (1.0 - s * f)
21
+ t = v * (1.0 - s * (1.0 - f))
22
+ i %= 6
23
+ if i == 0: return (v, t, p)
24
+ if i == 1: return (q, v, p)
25
+ if i == 2: return (p, v, t)
26
+ if i == 3: return (p, q, v)
27
+ if i == 4: return (t, p, v)
28
+ if i == 5: return (v, p, q)
29
+ return (0, 0, 0)
30
+
31
+ def _file_dialog_process(queue, initial_name):
32
+ """Helper process to show file dialog without crashing macOS Cocoa event loop."""
33
+ try:
34
+ import tkinter as tk
35
+ from tkinter import filedialog
36
+ root = tk.Tk()
37
+ root.withdraw()
38
+ root.attributes('-topmost', True)
39
+ file_path = filedialog.asksaveasfilename(
40
+ title="Save Watercolor Screenshot",
41
+ initialfile=initial_name,
42
+ defaultextension=".png",
43
+ filetypes=[("PNG Image", "*.png"), ("All files", "*.*")]
44
+ )
45
+ root.destroy()
46
+ queue.put(file_path)
47
+ except Exception as e:
48
+ print(f"Dialog Error: {e}")
49
+ queue.put(None)
50
+
51
+ def save_screenshot(img_data):
52
+ """Saves a screenshot after getting a path from a separate process dialog."""
53
+ initial_name = f"render_{int(time.time())}.png"
54
+
55
+ # We use a Queue to get the result from the child process
56
+ q = mp.Queue()
57
+ p = mp.Process(target=_file_dialog_process, args=(q, initial_name))
58
+ p.start()
59
+
60
+ # This will block the simulation for a moment while the user picks a file,
61
+ # which is desirable so they don't keep painting while the dialog is open.
62
+ file_path = q.get()
63
+ p.join()
64
+
65
+ if file_path:
66
+ try:
67
+ # Taichi fields are (width, height, 3) with origin at bottom-left.
68
+ # PIL expects (height, width, 3) with origin at top-left.
69
+ # Transpose (0, 1, 2) -> (1, 0, 2) and flip vertically.
70
+ corrected_img = np.flip(img_data.swapaxes(0, 1), 0)
71
+ PIL.Image.fromarray(corrected_img).save(file_path)
72
+ print(f"Saved screenshot to: {file_path}")
73
+ except Exception as e:
74
+ print(f"Failed to save image: {e}")
75
+ else:
76
+ print("Save cancelled.")
77
+
78
+ def launch_viewer():
79
+ import argparse
80
+ from dataclasses import fields
81
+
82
+ parser = argparse.ArgumentParser(description="Watercolor Simulator: High Quality GPU Mode")
83
+ parser.add_argument("-r", "--res", type=int, default=1024, help="Simulation resolution (default: 1024)")
84
+ parser.add_argument("-f", "--fps", type=int, default=60, help="Target FPS cap (default: 60)")
85
+ parser.add_argument("-s", "--substeps", type=int, default=2, help="Max simulation substeps when painting (default: 2)")
86
+ parser.add_argument("-d", "--decimate", type=int, default=1, help="Render decimation (default: 1)")
87
+
88
+ # Add SimParams as arguments automatically
89
+ for f in fields(SimParams):
90
+ if f.name == 'color_rgb': continue # Skip complex types for CLI for now
91
+ arg_name = f.name.replace('_', '-')
92
+ # Use float for numeric types to be safe
93
+ arg_type = type(f.default) if f.default is not None else float
94
+ parser.add_argument(f"--{arg_name}", type=arg_type, default=f.default, help=f.metadata.get('help', ''))
95
+
96
+ args = parser.parse_args()
97
+
98
+ RES = args.res
99
+ arch = "gpu"
100
+
101
+ print(f"\n[SightOfSound] Starting Watercolor (High Quality GPU Mode)")
102
+ print(f" - Resolution: {RES}x{RES}")
103
+ print(f" - Backend: {arch.upper()}")
104
+ print(f" - FPS Cap: {args.fps}")
105
+ print(f" - Substeps: {args.substeps} (adaptive)")
106
+ print(f"--------------------------------")
107
+
108
+ # Initialize Engine
109
+ engine = WatercolorEngine(res=RES, arch=arch)
110
+
111
+ # Update engine params from CLI
112
+ cli_params = {}
113
+ for f in fields(SimParams):
114
+ if hasattr(args, f.name):
115
+ cli_params[f.name] = getattr(args, f.name)
116
+ engine.update_params(**cli_params)
117
+
118
+ window = ti.ui.Window("Watercolor: GPU (Fluid Simulation)", (RES, RES))
119
+ canvas = window.get_canvas()
120
+ gui = window.get_gui()
121
+
122
+ brush_id = 0
123
+ dryness = 0.5
124
+
125
+ last_paint_pos = None
126
+ last_paint_time = 0
127
+ paint_interval = 1.0 / 60.0
128
+ stroke_dist_accum = 0.0
129
+
130
+ # Kernel pacing stats
131
+ total_stamps = 0
132
+ last_stat_time = time.time()
133
+ frame_idx = 0
134
+ show_advanced = False
135
+
136
+ print("\n[Controls]")
137
+ print(" - Mouse Left (LMB): Paint with bleeding/dripping water")
138
+ print(" - Space: Clear Canvas")
139
+ print(" - K: Save Screenshot")
140
+ print(" - UI: Use the 'Controls' sidebar (top-left) for all sliders")
141
+ print(" - Shortcuts: [ / ] for Size, D / F for Dryness")
142
+
143
+ start_time = time.time()
144
+ fps_limit = args.fps
145
+
146
+ # UI State
147
+ show_ui = True
148
+ paint_enabled = True
149
+
150
+ while window.running:
151
+ frame_start = time.time()
152
+ curr_time = frame_start - start_time
153
+ stamps_this_frame = 0 # Throttling cap
154
+
155
+ is_painting = window.is_pressed(ti.ui.LMB)
156
+ safe_mode = window.is_pressed(ti.ui.SHIFT)
157
+
158
+ # Handle events
159
+ events = window.get_events(ti.ui.PRESS)
160
+ for e in events:
161
+ if e.key == ti.ui.SPACE:
162
+ engine.clear()
163
+ elif e.key == 'b':
164
+ brush_id = (brush_id + 1) % 2
165
+ print(f"Brush ID: {brush_id}")
166
+ elif e.key == 'd':
167
+ dryness = max(0.0, dryness - 0.1)
168
+ elif e.key == 'f':
169
+ dryness = min(1.0, dryness + 0.1)
170
+ elif e.key == '[':
171
+ engine.update_params(brush_radius=max(SimParams.__dataclass_fields__['brush_radius'].metadata['min'], engine.p.brush_radius - 5))
172
+ elif e.key == ']':
173
+ engine.update_params(brush_radius=min(SimParams.__dataclass_fields__['brush_radius'].metadata['max'], engine.p.brush_radius + 5))
174
+ elif e.key == 'k':
175
+ # Force full render for screenshot and open dialog
176
+ engine.render(full_res=True)
177
+ img = engine._img_u8.to_numpy()
178
+ save_screenshot(img)
179
+ elif e.key == 'p':
180
+ paint_enabled = not paint_enabled
181
+ elif e.key == ti.ui.TAB:
182
+ show_ui = not show_ui
183
+ elif e.key == ti.ui.ESCAPE:
184
+ window.running = False
185
+
186
+ if is_painting and paint_enabled and not safe_mode:
187
+ # ti.ui.Window (GGUI) uses [0,1] with origin at BOTTOM-LEFT.
188
+ mx, my = window.get_cursor_pos()
189
+
190
+ # Hit-test logic removed to allow movable UI.
191
+ # Use 'Shift' guard or 'Tab' toggle to manage UI interactions.
192
+ px, py = mx * RES, my * RES
193
+
194
+ if last_paint_pos is not None:
195
+ lx, ly = last_paint_pos
196
+ dx, dy = px - lx, py - ly
197
+ dist = (dx*dx + dy*dy)**0.5
198
+
199
+ spacing = max(1.0, engine.p.brush_radius * 0.8)
200
+ stroke_dist_accum += dist
201
+
202
+ # 1. Throttling: distance-based and time-based
203
+ if dist > 1e-4: # Ignore absolute stillness
204
+ if stroke_dist_accum >= spacing or (curr_time - last_paint_time > paint_interval):
205
+ # Use a single stamp per event to keep stamps/sec <= 60
206
+ if stamps_this_frame < 1:
207
+ engine.paint_brush(px, py, engine.p.brush_radius, brush_id=brush_id, dryness=dryness)
208
+ stamps_this_frame += 1
209
+ total_stamps += 1
210
+
211
+ stroke_dist_accum = 0.0
212
+ last_paint_time = curr_time
213
+
214
+ last_paint_pos = (px, py)
215
+ else:
216
+ # Initial click stamp
217
+ engine.paint_brush(px, py, engine.p.brush_radius, brush_id=brush_id, dryness=dryness)
218
+ stamps_this_frame += 1
219
+ total_stamps += 1
220
+ last_paint_pos = (px, py)
221
+ last_paint_time = curr_time
222
+ else:
223
+ last_paint_pos = None
224
+ stroke_dist_accum = 0.0
225
+
226
+ # UI Sidebar (Overlay)
227
+ if show_ui:
228
+ with gui.sub_window("Controls", 0.05, 0.05, 0.3, 0.9) as w:
229
+ gui.text("Simulation Controls")
230
+ gui.text(f"Brush: {'ON' if paint_enabled else 'PAUSED'} [P]")
231
+ gui.text("Toggle UI: [Tab]")
232
+ gui.text("Safe Move: [Hold Shift]")
233
+
234
+ if gui.button("Clear Canvas"): engine.clear()
235
+
236
+ gui.text("--- Brush & Color ---")
237
+ brush_id = gui.slider_int("Brush Type", brush_id, 0, 1)
238
+ dryness = gui.slider_float("Brush Dryness", dryness, 0.0, 1.0)
239
+ engine.p.color_rgb = gui.color_edit_3("Color Picker", engine.p.color_rgb)
240
+
241
+ # Use dataclass fields to build the UI dynamically
242
+ from dataclasses import fields
243
+
244
+ for f in fields(SimParams):
245
+ cat = f.metadata.get("category", "Normal")
246
+ if cat == "Normal":
247
+ if f.name in ["color_rgb"]: continue # Handled specially
248
+
249
+ display_name = f.name.replace("_", " ").title()
250
+ val = getattr(engine.p, f.name)
251
+ new_val = gui.slider_float(display_name, val, f.metadata.get('min', 0.0), f.metadata.get('max', 1.0))
252
+ setattr(engine.p, f.name, new_val)
253
+
254
+ gui.text("--- Advanced ---")
255
+ show_advanced = gui.checkbox("Advanced Settings", show_advanced)
256
+
257
+ if show_advanced:
258
+ for f in fields(SimParams):
259
+ cat = f.metadata.get("category")
260
+ if cat == "Advanced":
261
+ display_name = f.name.replace("_", " ").title()
262
+ val = getattr(engine.p, f.name)
263
+ new_val = gui.slider_float(display_name, val, f.metadata.get('min', 0.0), f.metadata.get('max', 1.0))
264
+ setattr(engine.p, f.name, new_val)
265
+
266
+ if gui.button("Save Screenshot"):
267
+ engine.render(full_res=True)
268
+ img = engine._img_u8.to_numpy()
269
+ save_screenshot(img)
270
+
271
+ # Sync GUI to Engine
272
+ engine.update_params()
273
+
274
+ # Performance stats monitor
275
+ now = time.time()
276
+ if now - last_stat_time > 2.0:
277
+ fps_val = 1.0/elapsed if 'elapsed' in locals() and elapsed > 0 else 0
278
+ print(f"[Stats] Stamps/sec: {total_stamps//2} | FPS: {fps_val:.1f} | Substeps: {current_substeps}")
279
+ total_stamps = 0
280
+ last_stat_time = now
281
+
282
+ # Consistent Simulation Substeps (Fixed speed regardless of interaction)
283
+ current_substeps = args.substeps
284
+ for _ in range(current_substeps):
285
+ engine.step()
286
+
287
+ # Render directly from field with decimation
288
+ if (frame_idx % args.decimate == 0):
289
+ engine.render()
290
+ canvas.set_image(engine._img)
291
+
292
+ window.show()
293
+ frame_idx += 1
294
+
295
+ # Enforce FPS cap to prevent resource hogging
296
+ elapsed = time.time() - frame_start
297
+ if elapsed < 1.0 / fps_limit:
298
+ time.sleep(1.0 / fps_limit - elapsed)
299
+
300
+ def main():
301
+ """Main entry point that launches the viewer."""
302
+ launch_viewer()
303
+
304
+ if __name__ == "__main__":
305
+ main()
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: open-watercolor-sim
3
+ Version: 0.1.1
4
+ Summary: A high-performance GPU-accelerated watercolor simulation engine using Taichi.
5
+ Author-email: Shuoqichen <samchenmgx@gmail.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/shuoqichen/Open-Watercolor-Sim
8
+ Keywords: watercolor,simulation,computer graphics,real-time,taichi,gpu,digital painting
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: taichi>=1.7.4
16
+ Requires-Dist: numpy>=2.0.0
17
+ Requires-Dist: pillow>=10.0.0
18
+ Dynamic: license-file
19
+
20
+ # Open-Watercolor-Sim
21
+
22
+ **Open-Watercolor-Sim** is a real-time GPU-accelerated watercolor simulation framework in Python. It provides a physically inspired simulation engine designed for both artistic creation and as a research toolbox for computer graphics and fluid dynamics exploration.
23
+
24
+ ## Demo
25
+
26
+ ![Watercolor Simulation](https://raw.githubusercontent.com/shuoqichen/Open-Watercolor-Sim/main/demo/demo.png)
27
+
28
+ ## Features
29
+
30
+ - **GPU Acceleration**: Built on [Taichi Lang](https://github.com/taichi-dev/taichi) for high-performance physics-based simulation.
31
+ - **Interactive Controls**: Real-time brush interaction with adjustable radius, pressure (pigment load), and water release.
32
+ - **Physically Inspired Effects**: Models complex watercolor behaviors including diffusion, evaporation, edge darkening (coffee-ring effect), and granulation.
33
+ - **Cross-Platform**: Support for multiple GPU backends (CUDA, Metal, Vulkan) enabling real-time performance on Windows, macOS, and Linux.
34
+ - **Research Toolbox**: Easily extensible architecture for testing new fluid advection schemes or pigment interaction models.
35
+
36
+ ## Installation
37
+
38
+ **Open-Watercolor-Sim** requires Python 3.10 or newer.
39
+
40
+ ```bash
41
+ pip install open-watercolor-sim
42
+ ```
43
+
44
+ ## Quickstart
45
+
46
+ You can launch the interactive viewer using the console entry point:
47
+
48
+ ```bash
49
+ watercolor-sim
50
+ ```
51
+
52
+ Alternatively, run the module directly:
53
+
54
+ ```bash
55
+ python -m open_watercolor_sim.viewer
56
+ ```
57
+
58
+ ## Project Structure
59
+
60
+ ```text
61
+ demo/ # Previews and example outputs
62
+ src/open_watercolor_sim/ # Main package
63
+ ├── viewer.py # Interactive GGUI-based viewer
64
+ └── brush/
65
+ ├── configs.py # Simulation and artistic parameters
66
+ └── watercolor_engine.py # Core Taichi-based simulation logic
67
+ ```
68
+
69
+ ## Citation
70
+
71
+ If you use this framework in your research, please cite it using the following BibTeX:
72
+
73
+ ```bibtex
74
+ @software{open_watercolor_sim,
75
+ title = {Open-Watercolor-Sim: A Real-Time GPU-Accelerated Watercolor Simulation Framework},
76
+ author = {Chen, Shuoqi},
77
+ year = {2026},
78
+ url = {https://github.com/shuoqichen/Open-Watercolor-Sim}
79
+ }
80
+ ```
81
+
82
+ *Note: A DOI-backed citation for the paper-aligned release will be added in a future update.*
83
+
84
+ ## License
85
+
86
+ This project is licensed under the **Apache License 2.0**.
@@ -0,0 +1,11 @@
1
+ open_watercolor_sim/__init__.py,sha256=38mF1BAJw8EKekS3JoBcCmymrW8VMaPJVJTnajc9oNI,360
2
+ open_watercolor_sim/viewer.py,sha256=XEc-iZzCdQ2eoCry0WftA6sh5agKaixBEslaQVXk6b0,11836
3
+ open_watercolor_sim/brush/__init__.py,sha256=c3CV0Fx18zGp4tkikPEg8RWKe7Qh_3Y6xdlTwoQu4xA,148
4
+ open_watercolor_sim/brush/configs.py,sha256=OrOmmhkFTArrRrXUhcs2drW-OhQvlqQqXyB7qHZqM5g,5358
5
+ open_watercolor_sim/brush/watercolor_engine.py,sha256=fYh5irTw26qNL7X2Xek3_rlQkDGCoLBQol9sAwN-dcQ,39568
6
+ open_watercolor_sim-0.1.1.dist-info/licenses/LICENSE,sha256=hNTfVb-YuDaCbEbYICKxc1Jm6Hvx_oXKJNfyNfHdVis,8898
7
+ open_watercolor_sim-0.1.1.dist-info/METADATA,sha256=2pF267C-beHA9qsEx1V7xUHfhynVpkRcAjWw97jndBg,3036
8
+ open_watercolor_sim-0.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
9
+ open_watercolor_sim-0.1.1.dist-info/entry_points.txt,sha256=NKDM1FnPQRURLl5VNpHo1mmEz2NK1ms3xyUKrBuCf1c,67
10
+ open_watercolor_sim-0.1.1.dist-info/top_level.txt,sha256=XkjoEYSF0AQkJ7jKx8CPnjWhobUhxhgJXl_rIX_RnM0,20
11
+ open_watercolor_sim-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ watercolor-sim = open_watercolor_sim.viewer:main
@@ -0,0 +1,171 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but not
32
+ limited to compiled object code, generated documentation, and
33
+ conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including the
49
+ original version of the Work and any modifications or additions
50
+ to that Work that is intentionally submitted to the Licensor for
51
+ inclusion in the Work by the copyright owner or by an individual or
52
+ Legal Entity authorized to represent the copyright owner. For the
53
+ purposes of this definition, "submitted" means any form of
54
+ electronic, verbal, or written communication sent to the Licensor or
55
+ its representatives, including but not limited to communication on
56
+ electronic mailing lists, source code control systems, and issue
57
+ tracking systems that are managed by, or on behalf of, the Licensor
58
+ for the purpose of discussing and improving the Work, but excluding
59
+ communication that is conspicuously marked or otherwise designated
60
+ in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by the Licensor
64
+ and subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and
67
+ conditions of this License, each Contributor hereby grants to You
68
+ a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
69
+ irrevocable copyright license to reproduce, prepare Derivative
70
+ Works of, publicly display, publicly perform, sublicense,
71
+ and distribute the Work and such Derivative Works in Source or
72
+ Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and
75
+ conditions of this License, each Contributor hereby grants to You
76
+ a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
77
+ irrevocable (except as stated in this section) patent license to
78
+ make, have made, use, offer to sell, sell, import, and otherwise
79
+ transfer the Work, where such license applies only to those patent
80
+ claims licensable by such Contributor that are necessarily infringed
81
+ by their Contribution(s) alone or by combination of their
82
+ Contribution(s) with the Work to which such Contribution(s) was
83
+ submitted. If You institute patent litigation against any entity
84
+ (including a cross-claim or counter-claim in a lawsuit) alleging
85
+ that the Work or a Contribution incorporated within the Work
86
+ constitutes direct or contributory patent infringement, then any
87
+ patent licenses granted to You under this License for that Work
88
+ shall terminate as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies
91
+ of the Work or Derivative Works thereof in any medium, with or
92
+ without modifications, and in Source or Object form, provided that
93
+ You meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or Derivative
96
+ Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute
109
+ must include a readable copy of the notices contained
110
+ within such NOTICE file, excluding those notices that do
111
+ not pertain to any part of the Derivative Works, in at
112
+ least one of the following places: within a NOTICE text file
113
+ distributed as part of the Derivative Works; within the
114
+ Source form or documentation, if provided along with the
115
+ Derivative Works; or, within a display generated by the
116
+ Derivative Works, if and wherever such third-party notices
117
+ normally appear. The contents of the NOTICE file are for
118
+ informational purposes only and do not modify the License.
119
+
120
+ 5. Submission of Contributions. Unless You explicitly state
121
+ otherwise, any Contribution intentionally submitted for
122
+ inclusion in the Work by You to the Licensor shall be under
123
+ the terms and conditions of this License, without any
124
+ additional terms or conditions. Notwithstanding the above,
125
+ nothing herein shall supersede or modify the terms of any
126
+ separate license agreement you may have executed with the
127
+ Licensor regarding such Contributions.
128
+
129
+ 6. Trademarks. This License does not grant permission to use the
130
+ trade names, trademarks, service marks, or product names of the
131
+ Licensor, except as required for reasonable and customary use in
132
+ describing the origin of the Work and reproducing the content of
133
+ the NOTICE file.
134
+
135
+ 7. Disclaimer of Warranty. Unless required by applicable law
136
+ or agreed to in writing, Licensor provides the Work (and
137
+ each Contributor provides its Contributions) on an "AS IS"
138
+ BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
139
+ express or implied, including, without limitation, any
140
+ warranties or conditions of TITLE, NON-INFRINGEMENT,
141
+ MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
142
+
143
+ 8. Limitation of Liability. In no event and under no legal
144
+ theory, whether in tort (including negligence), contract,
145
+ or otherwise, unless required by applicable law (such as
146
+ deliberate and grossly negligent acts) or agreed to in writing,
147
+ shall any Contributor be liable to You for damages,
148
+ including any direct, indirect, special, incidental,
149
+ or consequential damages of any character arising as a
150
+ result of this License or out of the use or inability to use
151
+ the Work (including but not limited to damages for loss of
152
+ goodwill, work stoppage, computer failure or malfunction, or
153
+ any and all other commercial damages or losses), even if
154
+ such Contributor has been advised of the possibility of such
155
+ damages.
156
+
157
+ 9. Accepting Warranty or Additional Liability. While
158
+ redistributing the Work or Derivative Works thereof, You may
159
+ choose to offer, and charge a fee for, acceptance of support,
160
+ warranty, indemnity, or other liability obligations and/or
161
+ rights consistent with this License. However, in accepting
162
+ such obligations, You may act only on Your own behalf and on
163
+ Your sole responsibility, not on behalf of any other Contributor,
164
+ and only if You agree to indemnify, defend, and hold each
165
+ Contributor harmless for any liability incurred by, or claims
166
+ asserted against, such Contributor by reason of your accepting
167
+ such warranty or additional liability.
168
+
169
+ END OF TERMS AND CONDITIONS
170
+
171
+ Copyright (c) 2026 Shuoqi Chen
@@ -0,0 +1 @@
1
+ open_watercolor_sim