diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 95adb972dd6..c518b15f97c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,6 +28,7 @@ repos: rev: 25.1.0 hooks: - id: black + exclude: core/tests args: ["-l", "88", "--skip-string-normalization"] - repo: https://github.com/codespell-project/codespell @@ -42,10 +43,11 @@ repos: rev: v0.9.6 hooks: - id: ruff + exclude: core/tests - repo: https://github.com/hoodmane/pyscript-prettier-precommit rev: "v3.0.0-alpha.6" hooks: - id: prettier - exclude: core/test|core/dist|core/types|core/src/stdlib/pyscript.js|pyscript\.sw/|core/src/3rd-party + exclude: core/tests|core/dist|core/types|core/src/stdlib/pyscript.js|pyscript\.sw/|core/src/3rd-party args: [--tab-width, "4"] diff --git a/core/package-lock.json b/core/package-lock.json index 14faa9c24de..9da8de5ceb4 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1,19 +1,19 @@ { "name": "@pyscript/core", - "version": "0.6.33", + "version": "0.6.35", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pyscript/core", - "version": "0.6.33", + "version": "0.6.35", "license": "APACHE-2.0", "dependencies": { "@ungap/with-resolvers": "^0.1.0", "@webreflection/idb-map": "^0.3.2", "add-promise-listener": "^0.1.3", "basic-devtools": "^0.1.6", - "polyscript": "^0.16.15", + "polyscript": "^0.16.17", "sabayon": "^0.6.6", "sticky-module": "^0.1.1", "to-json-callback": "^0.1.1", @@ -1734,9 +1734,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.105", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.105.tgz", - "integrity": "sha512-ccp7LocdXx3yBhwiG0qTQ7XFrK48Ua2pxIxBdJO8cbddp/MvbBtPFzvnTchtyHQTsgqqczO8cdmAIbpMa0u2+g==", + "version": "1.5.107", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.107.tgz", + "integrity": "sha512-dJr1o6yCntRkXElnhsHh1bAV19bo/hKyFf7tCcWgpXbuFIF0Lakjgqv5LRfSDaNzAII8Fnxg2tqgHkgCvxdbxw==", "dev": true, "license": "ISC" }, @@ -2747,9 +2747,9 @@ } }, "node_modules/polyscript": { - "version": "0.16.15", - "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.16.15.tgz", - "integrity": "sha512-E2rFTz1CzqFpuA4ALdko6FJNeQ4Bb3EAZHhcx4bFqc6zB188iqJ+GJM0pcsZUqn/ExZpvd8CPLqHIyaNDRW2SQ==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.16.17.tgz", + "integrity": "sha512-6dWyWoZn6Z915bizfjftc82p2BhkWn6pPgd/u9GBXmdXTZBPC1ezDizqtgwpceTK6GNHPuNQ4elLWr5G0W+Mrw==", "license": "APACHE-2.0", "dependencies": { "@ungap/structured-clone": "^1.3.0", @@ -3913,9 +3913,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { diff --git a/core/package.json b/core/package.json index c761ac476e8..5e921cbba4d 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@pyscript/core", - "version": "0.6.33", + "version": "0.6.35", "type": "module", "description": "PyScript", "module": "./index.js", @@ -66,7 +66,7 @@ "@webreflection/idb-map": "^0.3.2", "add-promise-listener": "^0.1.3", "basic-devtools": "^0.1.6", - "polyscript": "^0.16.15", + "polyscript": "^0.16.17", "sabayon": "^0.6.6", "sticky-module": "^0.1.1", "to-json-callback": "^0.1.1", diff --git a/core/tests/index.html b/core/tests/index.html index d62386b944d..7ea784bae4d 100644 --- a/core/tests/index.html +++ b/core/tests/index.html @@ -14,5 +14,5 @@ a:hover { opacity: 1; } - + diff --git a/core/tests/manual/issue-2302/assets/genuary25-18.m4a b/core/tests/manual/issue-2302/assets/genuary25-18.m4a new file mode 100644 index 00000000000..aa10617e4aa Binary files /dev/null and b/core/tests/manual/issue-2302/assets/genuary25-18.m4a differ diff --git a/core/tests/manual/issue-2302/glue/multipyjs.py b/core/tests/manual/issue-2302/glue/multipyjs.py new file mode 100644 index 00000000000..dd483f52255 --- /dev/null +++ b/core/tests/manual/issue-2302/glue/multipyjs.py @@ -0,0 +1,20 @@ +from pyscript import config + +MICROPYTHON = config["type"] == "mpy" + +if MICROPYTHON: + def new(obj, *args, **kwargs): + return obj.new(*args, kwargs) if kwargs else obj.new(*args) + def call(obj, *args, **kwargs): + return obj(*args, kwargs) if kwargs else obj(*args) +else: + def new(obj, *args, **kwargs): + return obj.new(*args, **kwargs) + def call(obj, *args, **kwargs): + return obj(*args, **kwargs) + +if not MICROPYTHON: + import pyodide_js + pyodide_js.setDebug(True) + +from pyscript.ffi import to_js, create_proxy diff --git a/core/tests/manual/issue-2302/glue/perlin-0.0.0-cp312-cp312-pyodide_2024_0_wasm32.whl b/core/tests/manual/issue-2302/glue/perlin-0.0.0-cp312-cp312-pyodide_2024_0_wasm32.whl new file mode 100644 index 00000000000..718ace2e725 Binary files /dev/null and b/core/tests/manual/issue-2302/glue/perlin-0.0.0-cp312-cp312-pyodide_2024_0_wasm32.whl differ diff --git a/core/tests/manual/issue-2302/index.html b/core/tests/manual/issue-2302/index.html new file mode 100644 index 00000000000..a2ec422b739 --- /dev/null +++ b/core/tests/manual/issue-2302/index.html @@ -0,0 +1,69 @@ + + + + Genuary + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ + + diff --git a/core/tests/manual/issue-2302/libfft.py b/core/tests/manual/issue-2302/libfft.py new file mode 100644 index 00000000000..226842800c6 --- /dev/null +++ b/core/tests/manual/issue-2302/libfft.py @@ -0,0 +1,83 @@ +from dataclasses import dataclass, field +import sys + +@dataclass +class BeatSync: + fft_res: int = field() + + on_beat: bool = False + beat: int = -1 + since_last_beat: float = sys.maxsize + + _prev: int = 0 + _count: int = 0 + _bins: list[int] = field(default_factory=list) + _last_detection: float = -1.0 + _threshold: int = 50 + _diff: int = 40 + _cooldown: float = 0.2 + + _highest: int = 0 + + def __post_init__(self): + self._bins = [int(13/16*self.fft_res/2)+17, int(13/16*self.fft_res/2)+18] + + def reset(self): + self.beat = -1 + self._prev = 0 + self._count = 0 + self._last_detection = -1.0 + self.since_last_beat = sys.maxsize + # print('bs reset') + + def update(self, data, running_time): + self._count += 1 + self.since_last_beat = running_time - self._last_detection + d = sum(data[bin] for bin in self._bins) + if d < self._threshold: + self.on_beat = False + elif d - self._prev < self._diff: + self.on_beat = False + elif self.since_last_beat < self._cooldown: + self.on_beat = False + else: + self._last_detection = running_time + self.since_last_beat = 0 + self.on_beat = True + self.beat += 1 + self._prev = d + +@dataclass +class FreqIntensity: + freq: float = field() + fft_res: int = field() + + intensity: float = 0.0 + intensity_slew: float = 0.0 + scale_min: float = 0.0 + scale_max: float = 350 + max: float = 0.0 + _sample_rate: int = 48000 + _bin_indexes: list[int] = field(default_factory=list) + _harmonics: int = 8 + _slew_factor: float = 0.8 + + def __post_init__(self): + self._bin_indexes = [ + round((harmonic+1) * self.freq / self._sample_rate * self.fft_res / 2) + for harmonic in range(self._harmonics) + ] + print(self._bin_indexes) + + def update(self, data): + intensity = 0.0 + for bin in range(self._harmonics): + intensity += data[self._bin_indexes[bin]]/(bin+1) + self.intensity = intensity + self.intensity_slew = self._slew_factor * self.intensity_slew + (1 - self._slew_factor) * intensity + self.max = max(intensity, self.max) + + @property + def intensity_scaled(self): + raw = max(0, min(1.0, (self.intensity_slew - self.scale_min)/(self.scale_max - self.scale_min))) + return raw * raw diff --git a/core/tests/manual/issue-2302/libthree.py b/core/tests/manual/issue-2302/libthree.py new file mode 100644 index 00000000000..2c0c675d528 --- /dev/null +++ b/core/tests/manual/issue-2302/libthree.py @@ -0,0 +1,189 @@ +import asyncio +from dataclasses import dataclass, field +from typing import Callable + +from pyscript import document, window + +from pyscript.js_modules import three as THREE +from pyscript.js_modules.stats_gl import default as StatsGL +from pyscript.js_modules import lsgeo, line2, linemat + +from multipyjs import MICROPYTHON, new, call, to_js, create_proxy + +@dataclass +class SoundPlayer: + sound: THREE.Audio = field() + on_start: Callable[[], None] = field() + on_stop: Callable[[], None] = field(default=lambda: None) + + _start_time: float = -1.0 + + def play(self): + self.sound.stop() + self.on_start() + self._start_time = self.sound.context.currentTime + self.sound.play() + + def stop(self): + self.sound.stop() + self.on_stop() + self._start_time = -1.0 + + def toggle(self): + if self.sound.isPlaying: + self.stop() + else: + self.play() + + @property + def running_time(self): + if self.sound.isPlaying: + return self.sound.context.currentTime - self._start_time + elif self._start_time != -1.0: + self.stop() + return 0.0 + +def get_renderer(): + renderer = new(THREE.WebGLRenderer, antialias=True) + renderer.setSize(window.innerWidth, window.innerHeight) + renderer.setPixelRatio(window.devicePixelRatio) + renderer.setClearColor(0xF5F0DC) + pyterms = list(document.getElementsByTagName("py-terminal")) + if pyterms: + pyterm = pyterms[0] + pyterm.parentNode.removeChild(pyterm) + document.getElementById("pyterm").appendChild(pyterm) + + document.getElementById("threejs").appendChild(renderer.domElement) + + initial = {0: "115px", 1: "calc(100vh - 120px)"} + @create_proxy + def split_element_style(dimension, size, gutter_size, index): + if index in initial: + result = {dimension: initial.pop(index)} + else: + result = {dimension: f"calc({int(size)}vh - {gutter_size}px)"} + return to_js(result) + + call( + window.Split, + ["#pyterm", "#threejs"], + direction="vertical", + elementStyle=split_element_style, + minSize=0, + maxSize=to_js([120, 10000]), + ) + return renderer + +def get_ortho_camera(view_size): + aspect_ratio = window.innerWidth / window.innerHeight + camera = new( + THREE.OrthographicCamera, + -view_size * aspect_ratio, # Left + view_size * aspect_ratio, # Right + view_size, # Top + -view_size, # Bottom + -view_size, # Near plane + view_size, # Far plane + ) + camera.updateProjectionMatrix() + camera.position.set(0, 0, 0) + return camera + +def get_loading_manager(): + loading_mgr = new(THREE.LoadingManager) + ev = asyncio.Event() + + @create_proxy + def on_start(url, itemsLoaded, itemsTotal): + print(f'[{itemsLoaded}/{itemsTotal}] Started loading file: {url}') + loading_mgr.onStart = on_start + + @create_proxy + def on_progress(url, itemsLoaded, itemsTotal): + print(f'[{itemsLoaded}/{itemsTotal}] Loading file: {url}') + loading_mgr.onProgress = on_progress + + @create_proxy + def on_error(url): + print(f'There was a problem loading {url}') + loading_mgr.onError = on_error + + @create_proxy + def on_load(): + print('Loading assets complete!') + ev.set() + loading_mgr.onLoad = on_load + + return loading_mgr, ev + + +def get_perspective_camera(): + aspect_ratio = window.innerWidth / window.innerHeight + camera = new( + THREE.PerspectiveCamera, + 45, # fov + aspect_ratio, + 0.25, # near plane + 300, # far plane + ) + camera.position.set(0, 0, 30) + return camera + +def get_stats_gl(renderer): + stats = new(StatsGL, trackGPU=True, horizontal=False) + stats.init(renderer) + stats.dom.style.removeProperty("left") + stats.dom.style.right = "90px" + document.getElementById("stats").appendChild(stats.dom) + return stats + +def bg_from_v(*vertices): + geometry = new(THREE.BufferGeometry) + vertices_f32a = new(Float32Array, vertices) + attr = new(THREE.Float32BufferAttribute, vertices_f32a, 3) + return geometry.setAttribute('position', attr) + +def bg_from_p(*points): + buf = new(THREE.BufferGeometry) + buf.setFromPoints( + [new(THREE.Vector3, p[0], p[1], p[2]) for p in points] + ) + return buf + +def clear(): + # toggle stats and terminal? + stats_style = document.getElementById("stats-off").style + if stats_style.display == "none": + # turn stuff back on + stats_style.removeProperty("display") + document.getElementById("pyterm").style.height = "115px" + document.getElementById("threejs").style.height = "calc(100vh - 120px)" + for e in document.getElementsByClassName("gutter"): + e.style.removeProperty("display") + for e in document.getElementsByClassName("xterm-helper-textarea"): + e.focus() + break + return + + # no longer focus on xterm + document.activeElement.blur() + # hide stats + document.getElementById("stats-off").style.display = "none" + # hide pyterm and split gutter + document.getElementById("pyterm").style.height = "0vh" + document.getElementById("threejs").style.height = "100vh" + for e in document.getElementsByClassName("gutter"): + e.style.display = "none" + # hide ltk ad + for e in document.getElementsByClassName("ltk-built-with"): + e.style.display = "none" + # hide pyscript ad + for e in document.getElementsByTagName("div"): + style = e.getAttribute("style") + if style and style.startswith("z-index:999"): + e.style.display = "none" + for e in document.getElementsByTagName("svg"): + style = e.getAttribute("style") + if style and style.startswith("z-index:999"): + e.style.display = "none" diff --git a/core/tests/manual/issue-2302/main.py b/core/tests/manual/issue-2302/main.py new file mode 100644 index 00000000000..f2a6abf5db9 --- /dev/null +++ b/core/tests/manual/issue-2302/main.py @@ -0,0 +1,285 @@ +print("Starting up...") + +from array import array +import asyncio +import math +import time + +from pyscript import document, window, PyWorker + +from libthree import THREE, clear, SoundPlayer +from libthree import get_renderer, get_ortho_camera +from libthree import get_loading_manager, get_stats_gl +from libthree import lsgeo, line2, linemat, lsgeo +from libfft import BeatSync + +from multipyjs import MICROPYTHON, new, call, to_js, create_proxy + +from js import Float32Array + +scene = new(THREE.Scene) + +view_size = 1 +renderer = get_renderer() +camera = get_ortho_camera(view_size) +loading_mgr, loaded_event = get_loading_manager() + +t_loader = new(THREE.TextureLoader, loading_mgr) +t_loader.setPath('assets/') + +light = new(THREE.AmbientLight, 0xffffff, 1.0) +scene.add(light) + +fft_res = 2048 +audio_listener = new(THREE.AudioListener) +camera.add(audio_listener) +sound = new(THREE.Audio, audio_listener) +audio_loader = new(THREE.AudioLoader, loading_mgr) +analyser = new(THREE.AudioAnalyser, sound, fft_res) + +@create_proxy +def on_audio_load(buffer): + sound.setBuffer(buffer) + sound.setVolume(0.9) + sound.setLoop(False) + +audio_loader.load("assets/genuary25-18.m4a", on_audio_load) + +spheres = new(THREE.Group) +scene.add(spheres) + +line_basic_mat = new( + THREE.LineBasicMaterial, + color=0xffffff, +) + +zero_mat = new( + linemat.LineMaterial, + color=0x662503, + linewidth=3, +) + +other_mat = new( + linemat.LineMaterial, + color=0x662503, + linewidth=1.5, +) + +grid_mat = new( + linemat.LineMaterial, + color=0x662503, + linewidth=1, + dashed=True, + dashScale=1, + dashSize=0.5, + gapSize=1, + dashOffset=0, +) + +lines = [new(THREE.Group), new(THREE.Group)] +scene.add(lines[0]) +scene.add(lines[1]) + +def draw_lines(line_coords, mat_name, spy=False): + if spy: + line_coords_f32a = new(Float32Array, line_coords.length) + _it = line_coords.items + for i in range(line_coords.length): + line_coords_f32a[i] = _it[i] + else: + line_coords_f32a = new(Float32Array, line_coords) + if mat_name == 'zero': + mat = zero_mat + elif mat_name == 'grid': + mat = grid_mat + else: + mat = other_mat + + geo = new(THREE.BufferGeometry) + geo.setAttribute('position', new(THREE.BufferAttribute, line_coords_f32a, 3)) + seg = new(THREE.LineSegments, geo, line_basic_mat) + + lsg = new(lsgeo.LineSegmentsGeometry) + lsg.fromLineSegments(seg) + l1 = new(line2.Line2, lsg, mat) + l1.computeLineDistances() + l2 = new(line2.Line2, lsg, mat) + l2.computeLineDistances() + lines[0].add(l1) + lines[1].add(l2) + + seg.geometry.dispose() + del geo + del seg + +def drawing_done(): + maybe_with_spy = "with SPy" if USE_SPY else "with pure Python" + print(f"Time elapsed computing {maybe_with_spy}:", time.time() - start_ts) + drawing_event.set() + +grid_width = 0 +grid_height = 0 +scroll_offset = 0 +def scale_lines(grid_ws=None, grid_hs=None, offset=None): + global grid_width, grid_height, scroll_offset + + if grid_ws: + grid_width = grid_ws + else: + grid_ws = grid_width + + if grid_hs: + grid_height = grid_hs + else: + grid_hs = grid_height + + if offset: + scroll_offset = offset + else: + offset = scroll_offset + + scale = 2.04/grid_hs + lines[0].scale.set(scale, scale, scale) + lines[1].scale.set(scale, scale, scale) + lines[0].position.set((offset - grid_ws/2) * scale, -grid_hs/2 * scale, 0) + lines[1].position.set((offset + grid_ws/2) * scale, -grid_hs/2 * scale, 0) + +def append_p(lines, p1, p2): + lines.append(p1[0]) + lines.append(p1[1]) + lines.append(0) + lines.append(p2[0]) + lines.append(p2[1]) + lines.append(0) + +def initial_calc(): + grid_w = int(1920 * 4) + grid_h = 1080 * 2 + grid_scale = 10 + noise_factor = 500 + grid_hs = int(grid_h/grid_scale) + grid_ws = int(grid_w/grid_scale) + crossfade_range = int(grid_ws/12.5) + + def grid_lines(): + lines = array("d") + grid_goal = 24 + grid_size_i = int(round((grid_ws - crossfade_range) / grid_goal)) + grid_actual = (grid_ws - crossfade_range) / grid_size_i + for i in range(0, grid_size_i): + x = i * grid_actual + append_p(lines, (x, 0), (x, grid_hs)) + for y in range(0, grid_hs, grid_goal): + append_p(lines, (0, y), (grid_ws-crossfade_range, y)) + return lines + + import perlin + spy_perlin = perlin.lib + spy_perlin.init() + spy_perlin.seed(44) + scale_lines(grid_ws - crossfade_range, grid_hs) + print("Computing the height map") + spy_perlin.make_height_map(grid_ws, grid_hs) + spy_perlin.update_height_map(grid_ws, grid_hs, grid_scale / noise_factor, 0) + print("Cross-fading the height map") + spy_perlin.crossfade_height_map(grid_ws, grid_hs, crossfade_range) + print("Drawing grid") + draw_lines(grid_lines(), 'grid') + print("Marching squares") + draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0), 'zero', spy=True) + draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.3), 'positive', spy=True) + draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.3), 'negative', spy=True) + draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.45), 'positive', spy=True) + draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.45), 'negative', spy=True) + draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.6), 'positive', spy=True) + draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.6), 'negative', spy=True) + draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.8), 'negative', spy=True) + draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.8), 'positive', spy=True) + drawing_done() + +drawing_event = asyncio.Event() +start_ts = time.time() + +USE_SPY = True +if USE_SPY: + initial_calc() +else: + worker = PyWorker("./worker.py", type="pyodide", configURL="./pyscript.toml") + worker.sync.draw_lines = draw_lines + worker.sync.drawing_done = drawing_done + worker.sync.scale_lines = scale_lines + worker.sync.print = print + +@create_proxy +def on_tap(event): + clear() + player.toggle() +document.addEventListener("click", on_tap) + +@create_proxy +def on_key_down(event): + element = document.activeElement + _class = element.getAttribute("class") + in_xterm = element.tagName != "BODY" and _class and "xterm" in _class + + if event.code == "Backquote": + # Screenshot mode. + clear() + elif not in_xterm: + # Don't react to those bindings when typing code. + if event.code == "Space": + player.toggle() +document.addEventListener("keydown", on_key_down) + +@create_proxy +def on_window_resize(event): + aspect_ratio = window.innerWidth / window.innerHeight + if camera.type == "OrthographicCamera": + camera.left = -view_size * aspect_ratio + camera.right = view_size * aspect_ratio + camera.top = view_size + camera.bottom = -view_size + camera.updateProjectionMatrix() + elif camera.type == "PerspectiveCamera": + camera.aspect = window.innerWidth / window.innerHeight + camera.updateProjectionMatrix() + else: + raise ValueError("Unknown camera type") + renderer.setSize(window.innerWidth, window.innerHeight) + scale_lines() + +window.addEventListener("resize", on_window_resize) + +@create_proxy +def animate(now=0.0): + data = analyser.getFrequencyData()#.to_py() in Pyodide + audio_now = player.running_time + bs.update(data, audio_now) + + if grid_width: + offset = -((20 * audio_now) % grid_width) + scale_lines(offset=offset) + + renderer.render(scene, camera) + stats_gl.update() + +def reset(): + global scroll_offset + bs.reset() + scale_lines() + +def on_stop(): + global scroll_offset + bs.reset() + scale_lines() + +await loaded_event.wait() + +stats_gl = get_stats_gl(renderer) +player = SoundPlayer(sound=sound, on_start=reset, on_stop=on_stop) +bs = BeatSync(fft_res=fft_res) +renderer.setAnimationLoop(animate) +print("Waiting for the contours...") + +await drawing_event.wait() +print("Tap the map to start...") diff --git a/core/tests/manual/issue-2302/perlin_py.py b/core/tests/manual/issue-2302/perlin_py.py new file mode 100644 index 00000000000..8141e8f7545 --- /dev/null +++ b/core/tests/manual/issue-2302/perlin_py.py @@ -0,0 +1,110 @@ +# Translated from https://github.com/josephg/noisejs. +from libthree import THREE +from multipyjs import new + +class V3: + def __init__(self, x, y, z): + self.x = x + self.y = y + self.z = z + + def __repr__(self): + return f"V3({self.x}, {self.y}, {self.z})" + + def dot2(self, x, y): + return self.x * x + self.y * y + + def dot3(self, x, y, z): + return self.x * x + self.y * y + self.z * z + + def to_js(self, scale=1.0): + return new(THREE.Vector3, self.x * scale, self.y * scale, self.z * scale) + +PERM = [0] * 512 +V3_P = [0] * 512 # assigned V3s in seed() +P = [151, 160, 137, 91, 90, 15, + 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, + 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, + 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, + 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, + 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, + 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, + 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, + 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, + 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, + 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, + 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, + 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180] +V3_I = [V3(1, 1, 0), V3(-1, 1, 0), V3(1, -1, 0), V3(-1, -1, 0), + V3(1, 0, 1), V3(-1, 0, 1), V3(1, 0, -1), V3(-1, 0, -1), + V3(0, 1, 1), V3(0, -1, 1), V3(0, 1, -1), V3(0, -1, -1)] + +def seed(s): + if isinstance(s, float) and 0.0 < s < 1.0: + s *= 65536 + + s = int(s) + if s < 256: + s |= s << 8 + + for i in range(256): + if i & 1: + v = P[i] ^ (s & 255) + else: + v = P[i] ^ ((s >> 8) & 255) + + PERM[i] = PERM[i + 256] = v + V3_P[i] = V3_P[i + 256] = V3_I[v % 12] + +seed(0) + +def fade(t): + return t * t * t * (t * (t * 6 - 15) + 10) + +def lerp(a, b, t): + return (1 - t) * a + t * b + +def perlin3(x, y, z): + # grid cells + x_c = int(x) + y_c = int(y) + z_c = int(z) + # relative coords within the cell + x -= x_c + y -= y_c + z -= z_c + # wrap cells + x_c &= 255 + y_c &= 255 + z_c &= 255 + # noise contributions to corners + n000 = V3_P[x_c + PERM[y_c + PERM[z_c]]].dot3(x, y, z) + n001 = V3_P[x_c + PERM[y_c + PERM[z_c + 1]]].dot3(x, y, z - 1) + n010 = V3_P[x_c + PERM[y_c + 1 + PERM[z_c]]].dot3(x, y - 1, z) + n011 = V3_P[x_c + PERM[y_c + 1 + PERM[z_c + 1]]].dot3(x, y - 1, z - 1) + n100 = V3_P[x_c + 1 + PERM[y_c + PERM[z_c]]].dot3(x - 1, y, z) + n101 = V3_P[x_c + 1 + PERM[y_c + PERM[z_c + 1]]].dot3(x - 1, y, z - 1) + n110 = V3_P[x_c + 1 + PERM[y_c + 1 + PERM[z_c]]].dot3(x - 1, y - 1, z) + n111 = V3_P[x_c + 1 + PERM[y_c + 1 + PERM[z_c + 1]]].dot3(x - 1, y - 1, z - 1) + # fade curve + u = fade(x) + v = fade(y) + w = fade(z) + # interpolation + return lerp( + lerp(lerp(n000, n100, u), lerp(n001, n101, u), w), + lerp(lerp(n010, n110, u), lerp(n011, n111, u), w), + v, + ) + +def curl2(x, y, z): + # https://www.bit-101.com/2017/2021/07/curl-noise/ + delta = 0.01 + n1 = perlin3(x + delta, y, z) + n2 = perlin3(x - delta, y, z) + cy = -(n1 - n2) / (delta * 2) + n1 = perlin3(x, y + delta, z) + n2 = perlin3(x, y - delta, z) + cx = -(n1 - n2) / (delta * 2) + print(n1, n2) + return V3(cx, cy, 0) diff --git a/core/tests/manual/issue-2302/pyscript.toml b/core/tests/manual/issue-2302/pyscript.toml new file mode 100644 index 00000000000..36409b57f2f --- /dev/null +++ b/core/tests/manual/issue-2302/pyscript.toml @@ -0,0 +1,16 @@ +name = "Marching Squares with SPy Copy Copy" +packages = [ "cffi", "./glue/perlin-0.0.0-cp312-cp312-pyodide_2024_0_wasm32.whl",] + +[files] +"./libthree.py" = "" +"./libfft.py" = "" +"./perlin_py.py" = "" +"./worker.py" = "" +"./glue/multipyjs.py" = "./multipyjs.py" + +[js_modules.main] +"https://cdn.jsdelivr.net/npm/three@v0.173.0/build/three.module.js" = "three" +"https://cdn.jsdelivr.net/npm/three@v0.173.0/examples/jsm/lines/LineMaterial.js" = "linemat" +"https://cdn.jsdelivr.net/npm/three@v0.173.0/examples/jsm/lines/Line2.js" = "line2" +"https://cdn.jsdelivr.net/npm/three@v0.173.0/examples/jsm/lines/LineSegmentsGeometry.js" = "lsgeo" +"https://cdn.jsdelivr.net/npm/stats-gl@3.6.0/dist/main.js" = "stats_gl" diff --git a/core/tests/manual/issue-2302/worker.py b/core/tests/manual/issue-2302/worker.py new file mode 100644 index 00000000000..bc857738af8 --- /dev/null +++ b/core/tests/manual/issue-2302/worker.py @@ -0,0 +1,141 @@ +from array import array + +from pyscript import sync, window +from perlin_py import perlin3, seed + +grid_w = int(1920 * 4) +grid_h = 1080 * 2 +grid_scale = 10 +noise_factor = 500 +grid_hs = int(grid_h/grid_scale) +grid_ws = int(grid_w/grid_scale) +crossfade_range = int(grid_ws/12.5) +height_map = array("d", [0.0] * (grid_hs * grid_ws)) +edge_table = [ + (), # 0 + ((3, 2),), # 1 + ((2, 1),), # 2 + ((3, 1),), # 3 + ((0, 1),), # 4 + ((0, 3), (1, 2)), # 5 (ambiguous) + ((0, 2),), # 6 + ((0, 3),), # 7 + ((0, 3),), # 8 + ((0, 2),), # 9 + ((0, 1), (2, 3)), # 10 (ambiguous) + ((0, 1),), # 11 + ((3, 1),), # 12 + ((2, 1),), # 13 + ((3, 2),), # 14 + (), # 15 +] + +def update_height_map(z): + i = 0 + for y in range(0, grid_h, grid_scale): + for x in range(0, grid_w, grid_scale): + # 3 octaves of noise + n = perlin3(x/noise_factor, y/noise_factor, z) + n += 0.50 * perlin3(2*x/noise_factor, 2*y/noise_factor, z) + n += 0.25 * perlin3(4*x/noise_factor, 4*y/noise_factor, z) + height_map[i] = n + i += 1 + +def crossfade_height_map(): + for y in range(grid_hs): + for x in range(crossfade_range): + pos_i = y*grid_ws + x + neg_i = y*grid_ws + grid_ws - crossfade_range + x + weight = x/crossfade_range + old_pos = height_map[pos_i] + old_neg = height_map[neg_i] + height_map[neg_i] = height_map[pos_i] = weight * old_pos + (1.0 - weight) * old_neg + + +def _crossfade_height_map(): + for y in range(grid_hs): + for x in range(crossfade_range): + pos_i = y*grid_ws + x + neg_i = y*grid_ws + grid_ws - x - 1 + old_pos = height_map[pos_i] + old_neg = height_map[neg_i] + weight = 0.5 - x/crossfade_range/2 + height_map[pos_i] = (1.0 - weight) * old_pos + weight * old_neg + height_map[neg_i] = (1.0 - weight) * old_neg + weight * old_pos + +def interpolate(sq_threshold, v1, v2): + if v1 == v2: + return v1 + return (sq_threshold - v1) / (v2 - v1) + +stats = {'maxx': 0, 'maxy': 0, 'minx': 0, 'miny': 0} +def append_p(lines, p1, p2): + lines.append(p1[0]) + lines.append(p1[1]) + lines.append(0) + lines.append(p2[0]) + lines.append(p2[1]) + lines.append(0) + stats['maxy'] = max(p1[1], p2[1], stats['maxy']) + stats['miny'] = min(p1[1], p2[1], stats['miny']) + stats['maxx'] = max(p1[0], p2[0], stats['maxx']) + stats['minx'] = min(p1[0], p2[0], stats['minx']) + +def marching_squares(height_map, sq_threshold): + lines = array("d") + + for y in range(grid_hs-1): + for x in range(grid_ws-1): #cf + tl = height_map[y*grid_ws + x] + tr = height_map[y*grid_ws + x+1] + bl = height_map[(y+1)*grid_ws + x] + br = height_map[(y+1)*grid_ws + x+1] + + sq_idx = 0 + if tl > sq_threshold: + sq_idx |= 8 + if tr > sq_threshold: + sq_idx |= 4 + if br > sq_threshold: + sq_idx |= 2 + if bl > sq_threshold: + sq_idx |= 1 + + edge_points = [ + (x + interpolate(sq_threshold, tl, tr), y), + (x + 1, y + interpolate(sq_threshold, tr, br)), + (x + interpolate(sq_threshold, bl, br), y + 1), + (x, y + interpolate(sq_threshold, tl, bl)), + ] + + for a, b in edge_table[sq_idx]: + append_p(lines, edge_points[a], edge_points[b]) + + return lines + +def grid_lines(): + lines = array("d") + for x in range(0, grid_ws - crossfade_range, 26): + append_p(lines, (x, 0), (x, grid_hs)) + for y in range(0, grid_hs, 24): + append_p(lines, (0, y), (grid_ws-crossfade_range, y)) + return lines + +seed(44) +sync.scale_lines(grid_ws - crossfade_range, grid_hs) +sync.print("Computing the height map") +update_height_map(0) +sync.print("Cross-fading the height map") +crossfade_height_map() +sync.draw_lines(grid_lines(), 'grid') +sync.draw_lines(marching_squares(height_map, 0), 'zero') +sync.draw_lines(marching_squares(height_map, 0.3), 'positive') +sync.draw_lines(marching_squares(height_map, -0.3), 'negative') +sync.draw_lines(marching_squares(height_map, 0.45), 'positive') +sync.draw_lines(marching_squares(height_map, -0.45), 'negative') +sync.draw_lines(marching_squares(height_map, 0.6), 'positive') +sync.draw_lines(marching_squares(height_map, -0.6), 'negative') +sync.draw_lines(marching_squares(height_map, -0.8), 'negative') +sync.draw_lines(marching_squares(height_map, 0.8), 'positive') +print(stats) +sync.drawing_done()