Compare commits

...

6 Commits

Author SHA1 Message Date
Alejandro Alonso
c5f4872340 🐛 Fix WebGL context lost problems 2025-12-26 12:10:47 +01:00
Alejandro Alonso
a57011ec7b Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-23 13:35:27 +01:00
Alejandro Alonso
cb325282ec Merge pull request #7994 from penpot/alotor-fix-font-style
🐛 Fix problem when changing colors with multiple fonts
2025-12-23 07:34:41 +01:00
Andrey Antukh
01ecde3bfa Add the ability to add relations on penpot sdk (#7987)
*  Add the ability to add relations on penpot sdk

* 📎 Remove debug console log
2025-12-22 20:55:31 +01:00
Alonso Torres
4000ec8762 🐛 Fix problem resizing auto size layouts (#7995) 2025-12-22 20:17:11 +01:00
alonso.torres
bd580ab159 🐛 Fix problem when changing colors with multiple fonts 2025-12-22 17:14:37 +01:00
13 changed files with 260 additions and 20 deletions

View File

@@ -554,7 +554,7 @@
(when (features/active-feature? state "text-editor/v2")
(let [instance (:workspace-editor state)
styles (some-> (editor.v2/getCurrentStyle instance)
(styles/get-styles-from-style-declaration)
(styles/get-styles-from-style-declaration :removed-mixed true)
((comp update-node-fn migrate-node))
(styles/attrs->styles))]
(editor.v2/applyStylesToSelection instance styles)))))))

View File

@@ -238,12 +238,12 @@
:always
(ctm/resize scalev resize-origin shape-transform shape-transform-inverse)
(and (ctl/any-layout-immediate-child? objects shape)
(and (or (ctl/any-layout-immediate-child? objects shape) (ctl/any-layout? shape))
(not= (:layout-item-h-sizing shape) :fix)
^boolean change-width?)
(ctm/change-property :layout-item-h-sizing :fix)
(and (ctl/any-layout-immediate-child? objects shape)
(and (or (ctl/any-layout-immediate-child? objects shape) (ctl/any-layout? shape))
(not= (:layout-item-v-sizing shape) :fix)
^boolean change-height?)
(ctm/change-property :layout-item-v-sizing :fix)

View File

@@ -307,7 +307,7 @@
:title (tr "inspect.attributes.typography.font-family")
:on-click #(reset! open-selector? true)}
(cond
(= :multiple font-id)
(or (= :multiple font-id) (= "mixed" font-id))
"--"
(some? font)

View File

@@ -11,6 +11,7 @@
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.shapes :as gsh]
[app.common.logging :as log]
[app.common.types.color :as clr]
[app.common.types.component :as ctk]
[app.common.types.path :as path]
@@ -54,6 +55,7 @@
[app.main.ui.workspace.viewport.widgets :as widgets]
[app.render-wasm.api :as wasm.api]
[app.util.debug :as dbg]
[app.util.globals :as ug]
[app.util.text-editor :as ted]
[beicon.v2.core :as rx]
[promesa.core :as p]
@@ -133,6 +135,9 @@
canvas-init? (mf/use-state false)
initialized? (mf/use-state false)
;; Refs to store current values for context restoration
viewport-state-ref (mf/use-ref nil)
;; REFS
[viewport-ref
on-viewport-ref] (create-viewport-ref)
@@ -301,6 +306,8 @@
;; We think moving this out to a handler will make the render code
;; harder to follow through.
(mf/with-effect [page-id]
;; Reset initialized state when page changes
(reset! initialized? false)
(when-let [canvas (mf/ref-val canvas-ref)]
(->> wasm.api/module
(p/fmap (fn [ready?]
@@ -331,11 +338,33 @@
(when (and @canvas-init? preview-blend)
(wasm.api/request-render "with-effect")))
;; Update viewport state ref whenever values change
(mf/with-effect [base-objects zoom vbox background]
(mf/set-ref-val! viewport-state-ref {:base-objects base-objects
:zoom zoom
:vbox vbox
:background background}))
(mf/with-effect [@canvas-init? zoom vbox background]
(when (and @canvas-init? (not @initialized?))
(wasm.api/initialize-viewport base-objects zoom vbox background)
(reset! initialized? true)))
;; Listen for context restoration events - register only once
(mf/with-effect []
(let [listener (fn [_]
(log/info :hint "Context restored event received, resetting viewport initialization")
(let [state (mf/ref-val viewport-state-ref)]
(when state
(reset! initialized? false)
(wasm.api/initialize-viewport (:base-objects state)
(:zoom state)
(:vbox state)
(:background state))
(wasm.api/request-render "context-restored"))))]
(.addEventListener ug/document "penpot:wasm:context-restored" listener)
#(.removeEventListener ug/document "penpot:wasm:context-restored" listener)))
(mf/with-effect [focus]
(when (and @canvas-init? @initialized?)
(if (empty? focus)

View File

@@ -21,6 +21,7 @@
[app.common.types.text :as txt]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.render-wasm :as drw]
[app.main.refs :as refs]
[app.main.render :as render]
[app.main.store :as st]
@@ -1233,6 +1234,97 @@
(re-find #"(?i)edge" user-agent) :edge
:else :unknown)))))
(defn- initialize-wasm-context
"Initializes the WebGL context and WASM render engine.
Registers the context with Emscripten, sets up extensions, and initializes the render module."
[canvas context flags]
(let [gl (unchecked-get wasm/internal-module "GL")
handle (.registerContext ^js gl context #js {"majorVersion" 2})]
(.makeContextCurrent ^js gl handle)
(set! wasm/gl-context-handle handle)
(set! wasm/gl-context context)
;; Force the WEBGL_debug_renderer_info extension as emscripten does not enable it
(.getExtension context "WEBGL_debug_renderer_info")
;; Initialize Wasm Render Engine
(h/call wasm/internal-module "_init" (/ (.-width ^js canvas) dpr) (/ (.-height ^js canvas) dpr))
(h/call wasm/internal-module "_set_render_options" flags dpr)))
(defn- handle-context-lost
"Handler for WebGL context loss events. Attempts to restore the context manually if possible."
[canvas event]
(.preventDefault event)
(log/warn :hint "WebGL context lost")
;; Only mark as lost if this is the same canvas (not a page change)
;; During page changes, clear-canvas handles the cleanup
(when (exists? js/window)
(let [saved-canvas (.-penpotCanvas js/window)]
(when (identical? canvas saved-canvas)
(set! wasm/context-initialized? false)
(st/emit! (drw/context-lost))
;; Only try to restore manually if we have the lose extension
(when-let [lose-ext (.-penpotLoseContextExt js/window)]
;; Use setTimeout to allow the browser to process the context loss event
(js/setTimeout
(fn []
(try
;; Check if context is actually lost before trying to restore
(let [current-context (.getContext ^js canvas "webgl2" default-context-options)]
(cond
(nil? current-context)
;; No context available, try to restore
(do
(log/info :hint "No context available, attempting to restore manually")
(.restoreContext ^js lose-ext))
(.-isContextLost ^js current-context)
;; Context exists but is lost, try to restore
(do
(log/info :hint "Context is lost, attempting to restore manually")
(.restoreContext ^js lose-ext))
:else
;; Context is already restored, skip
(log/info :hint "Context already restored, skipping manual restoration")))
;; After restoreContext(), the browser should fire webglcontextrestored
;; which will trigger our restoration handler
(catch :default err
(log/warn :hint "Could not restore context manually" :error err))))
0))))))
(defn- handle-context-restored
"Handler for WebGL context restoration events. Re-initializes the context and WASM module."
[canvas gl flags]
(log/info :hint "WebGL context restored by browser, re-initializing")
;; Re-initialize the context after browser restoration
;; Use setTimeout to ensure the browser has fully restored the context
(js/setTimeout
(fn []
(let [restored-context (.getContext ^js canvas "webgl2" default-context-options)]
(when-not (nil? restored-context)
(try
;; Clean up old handle if exists
(when-let [old-handle wasm/gl-context-handle]
(try
(.deleteContext ^js gl old-handle)
(catch :default _)))
;; Re-initialize the context and WASM module
(initialize-wasm-context canvas restored-context flags)
(set! wasm/context-initialized? true)
(log/info :hint "WebGL context restored successfully")
(st/emit! (drw/context-restored))
(ug/dispatch! (ug/event "penpot:wasm:context-restored"))
;; Force a render after context restoration
(request-render "context-restored")
(catch :default err
(log/error :hint "Failed to restore WebGL context"
:error err)))))
0)))
(defn init-canvas-context
[canvas]
(let [gl (unchecked-get wasm/internal-module "GL")
@@ -1243,17 +1335,45 @@
browser (get-browser)
browser (sr/translate-browser browser)]
(when-not (nil? context)
(let [handle (.registerContext ^js gl context #js {"majorVersion" 2})]
(.makeContextCurrent ^js gl handle)
(set! wasm/gl-context-handle handle)
(set! wasm/gl-context context)
;; Initialize WebGL context and WASM render engine
(initialize-wasm-context canvas context flags)
;; Force the WEBGL_debug_renderer_info extension as emscripten does not enable it
(.getExtension context "WEBGL_debug_renderer_info")
;; Store reference to lose extension for manual restoration
(let [lose-ext (.getExtension context "WEBGL_lose_context")]
(when (and lose-ext (exists? js/window))
(set! (.-penpotLoseContextExt js/window) lose-ext)
(set! (.-penpotCanvas js/window) canvas)))
;; Remove existing event listeners if any (cleanup from previous initialization)
(when wasm/context-lost-handler
(.removeEventListener canvas "webglcontextlost" wasm/context-lost-handler))
(when wasm/context-restored-handler
(.removeEventListener canvas "webglcontextrestored" wasm/context-restored-handler))
;; Create and store event handlers for context loss and restoration
(let [lost-handler (fn [event]
(handle-context-lost canvas event))
restored-handler (fn [event]
(handle-context-restored canvas gl flags))]
(set! wasm/context-lost-handler lost-handler)
(set! wasm/context-restored-handler restored-handler)
;; Add event listeners for context loss and restoration
(.addEventListener canvas "webglcontextlost" lost-handler)
(.addEventListener canvas "webglcontextrestored" restored-handler))
;; Add debug helpers for reproducing context loss issues
(when (exists? js/window)
(set! (.-penpotDebugPanning js/window)
(fn []
(let [lose-ext (.getExtension context "WEBGL_lose_context")]
(when lose-ext
(js/setTimeout
(fn []
(log/warn :hint "Forcing context loss for testing")
(.loseContext lose-ext))
1000))))))
;; Initialize Wasm Render Engine
(h/call wasm/internal-module "_init" (/ (.-width ^js canvas) dpr) (/ (.-height ^js canvas) dpr))
(h/call wasm/internal-module "_set_render_options" flags dpr))
(set! wasm/context-initialized? true))
(h/call wasm/internal-module "_set_browser" browser)
@@ -1269,6 +1389,16 @@
(set! wasm/context-initialized? false)
(h/call wasm/internal-module "_clean_up")
;; Remove event listeners before cleaning up context
(when (and wasm/gl-context (.-canvas wasm/gl-context))
(let [canvas (.-canvas wasm/gl-context)]
(when wasm/context-lost-handler
(.removeEventListener canvas "webglcontextlost" wasm/context-lost-handler)
(set! wasm/context-lost-handler nil))
(when wasm/context-restored-handler
(.removeEventListener canvas "webglcontextrestored" wasm/context-restored-handler)
(set! wasm/context-restored-handler nil))))
;; Ensure the WebGL context is properly disposed so browsers do not keep
;; accumulating active contexts between page switches.
(when-let [gl (unchecked-get wasm/internal-module "GL")]

View File

@@ -11,6 +11,8 @@
(defonce internal-module #js {})
(defonce gl-context-handle nil)
(defonce gl-context nil)
(defonce context-lost-handler nil)
(defonce context-restored-handler nil)
(defonce serializers
#js {:blur-type shared/RawBlurType
:blend-mode shared/RawBlendMode

View File

@@ -187,19 +187,23 @@
style-value (normalize-style-value style-name v)]
(assoc acc style-name style-value)))) {} style-defaults)))
(def mixed-values #{:mixed :multiple "mixed" "multiple"})
(defn get-styles-from-style-declaration
"Returns a ClojureScript object compatible with text nodes"
[style-declaration]
[style-declaration & {:keys [removed-mixed] :or {removed-mixed false}}]
(reduce
(fn [acc k]
(if (contains? mapping k)
(let [style-name (get-style-name-as-css-variable k)
[_ style-decode] (get mapping k)
style-value (.getPropertyValue style-declaration style-name)]
(assoc acc k (style-decode style-value)))
(when (or (not removed-mixed) (not (contains? mixed-values style-value)))
(assoc acc k (style-decode style-value))))
(let [style-name (get-style-name k)
style-value (normalize-attr-value k (.getPropertyValue style-declaration style-name))]
(assoc acc k style-value)))) {} txt/text-style-attrs))
(when (or (not removed-mixed) (not (contains? mixed-values style-value)))
(assoc acc k style-value))))) {} txt/text-style-attrs))
(defn get-styles-from-event
"Returns a ClojureScript object compatible with text nodes"

View File

@@ -1,5 +1,11 @@
# CHANGELOG
## 1.2.0-RC1
- Add the ability to add relations (with `addRelation` method)
## 1.1.0
- Same as 1.1.0-RC2

View File

@@ -1,9 +1,9 @@
{
"name": "@penpot/library",
"version": "1.1.0",
"version": "1.2.0-RC1",
"license": "MPL-2.0",
"author": "Kaleidos INC",
"packageManager": "yarn@4.11.0+sha512.4e54aeace9141df2f0177c266b05ec50dc044638157dae128c471ba65994ac802122d7ab35bcd9e81641228b7dcf24867d28e750e0bcae8a05277d600008ad54",
"packageManager": "yarn@4.12.0+sha512.f45ab632439a67f8bc759bf32ead036a1f413287b9042726b7cc4818b7b49e14e9423ba49b18f9e06ea4941c1ad062385b1d8760a8d5091a1a31e5f6219afca8",
"type": "module",
"repository": {
"type": "git",

View File

@@ -0,0 +1,30 @@
import * as penpot from "#self";
import { writeFile, readFile } from "fs/promises";
(async function () {
const context = penpot.createBuildContext();
{
const file1 = context.addFile({ name: "Test File 1" });
const file2 = context.addFile({ name: "Test File 1" });
context.addRelation(file1, file2);
}
{
let result = await penpot.exportAsBytes(context);
await writeFile("sample-relations.zip", result);
}
})()
.catch((cause) => {
console.error(cause);
const innerCause = cause.cause;
if (innerCause) {
console.error("Inner cause:", innerCause);
}
process.exit(-1);
})
.finally(() => {
process.exit(0);
});

View File

@@ -87,7 +87,8 @@
(try
(let [params (-> params decode-params fb/decode-file)]
(-> (swap! state fb/add-file params)
(get ::fb/current-file-id)))
(get ::fb/current-file-id)
(dm/str)))
(catch :default cause
(handle-exception cause))))
@@ -273,6 +274,16 @@
(catch :default cause
(handle-exception cause))))
:addRelation
(fn [file-id library-id]
(let [file-id (uuid/parse file-id)
library-id (uuid/parse library-id)]
(if (and file-id library-id)
(do
(swap! state update :relations assoc file-id library-id)
true)
false)))
:genId
(fn []
(dm/str (uuid/next)))

View File

@@ -194,7 +194,8 @@
:generated-by "penpot-library/%version%"
:referer (get opts :referer)
:files files
:relations []}
:relations (->> (:relations state)
(mapv vec))}
params (d/without-nils params)]
["manifest.json"

View File

@@ -54,6 +54,33 @@ test("create context with two file", () => {
assert.equal(file.data.pages.length, 0)
});
test("create context with two file and relation between", () => {
const context = penpot.createBuildContext();
const fileId_1 = context.addFile({name: "sample 1"});
const fileId_2 = context.addFile({name: "sample 2"});
context.addRelation(fileId_1, fileId_2);
const internalState = context.getInternalState();
assert.ok(internalState.files[fileId_1]);
assert.ok(internalState.files[fileId_2]);
assert.equal(internalState.files[fileId_1].name, "sample 1");
assert.equal(internalState.files[fileId_2].name, "sample 2");
assert.ok(internalState.relations[fileId_1]);
assert.equal(internalState.relations[fileId_1], fileId_2);
const file = internalState.files[fileId_2];
assert.ok(file.data);
assert.ok(file.data.pages);
assert.ok(file.data.pagesIndex);
assert.equal(file.data.pages.length, 0)
});
test("create context with file and page", () => {
const context = penpot.createBuildContext();