This commit is contained in:
Alejandro Alonso
2025-12-05 11:14:19 +01:00
parent 871c6bcd54
commit 0e03265abc
3 changed files with 555 additions and 29 deletions

View File

@@ -24,6 +24,8 @@
[app.common.types.path :as path]
[app.common.types.path.segment :as path.segm]
[app.common.types.shape :as cts]
[app.common.types.shape.blur :as ctsb]
[app.common.types.shape.shadow :as ctss]
[app.common.uuid :as uuid]
[cuerdas.core :as str]))
@@ -82,6 +84,182 @@
(declare create-svg-children)
(declare parse-svg-element)
(defn- process-gradient-stops
"Processes gradient stops to extract stop-color and stop-opacity from style attributes
and convert them to direct attributes. This ensures stops with style='stop-color:#...;stop-opacity:1'
are properly converted to stop-color and stop-opacity attributes."
[stops]
(mapv (fn [stop]
(let [stop-attrs (:attrs stop)
stop-style (get stop-attrs :style)
;; Parse style if it's a string - use the same logic as format-styles
parsed-style (when (and (string? stop-style) (seq stop-style))
(reduce (fn [res item]
(let [[k v] (-> (str/trim item) (str/split ":" 2))
k (keyword k)]
(assoc res k v)))
{}
(str/split stop-style ";")))
;; Extract stop-color and stop-opacity from style
style-stop-color (when parsed-style (:stop-color parsed-style))
style-stop-opacity (when parsed-style (:stop-opacity parsed-style))
;; Merge: use direct attributes first, then style values as fallback
final-attrs (cond-> stop-attrs
(and style-stop-color (not (contains? stop-attrs :stop-color)))
(assoc :stop-color style-stop-color)
(and style-stop-opacity (not (contains? stop-attrs :stop-opacity)))
(assoc :stop-opacity style-stop-opacity)
;; Remove style attribute if we've extracted its values
(or style-stop-color style-stop-opacity)
(dissoc :style))]
#?(:cljs (when (or style-stop-color style-stop-opacity)
(js/console.log "[process-gradient-stops] Extracted from style - stop-color:" style-stop-color "stop-opacity:" style-stop-opacity
"final attrs:" (clj->js final-attrs)))
:clj nil)
(assoc stop :attrs final-attrs)))
stops))
(defn- resolve-gradient-href
"Resolves xlink:href references in gradients by merging the referenced gradient's
stops and attributes with the referencing gradient. This ensures gradients that
reference other gradients (like linearGradient3550 referencing linearGradient3536)
inherit the stops from the base gradient.
According to SVG spec, when a gradient has xlink:href:
- It inherits all attributes from the referenced gradient
- It inherits all stops from the referenced gradient
- The referencing gradient's attributes override the base ones
- If the referencing gradient has stops, they replace the base stops
Returns the defs map with all gradient href references resolved."
[defs]
#?(:cljs (js/console.log "[resolve-gradient-href] Starting resolution for" (count defs) "defs")
:clj nil)
(letfn [(resolve-gradient [gradient-id gradient-node defs visited]
(if (contains? visited gradient-id)
(do
#?(:cljs (js/console.warn "[resolve-gradient] Circular reference detected for" gradient-id)
:clj nil)
gradient-node) ;; Avoid circular references
(let [attrs (:attrs gradient-node)
href-id (or (:href attrs) (:xlink:href attrs))
href-id (when (and (string? href-id) (pos? (count href-id)))
(subs href-id 1)) ;; Remove leading #
base-gradient (when (and href-id (contains? defs href-id))
(get defs href-id))
_ #?(:cljs (when href-id
(js/console.log "[resolve-gradient] Looking for base" href-id
"in defs:" (clj->js (keys defs))
"found?:" (contains? defs href-id)
"base-gradient:" (some? base-gradient)))
:clj nil)
resolved-base (when base-gradient
(do
#?(:cljs (js/console.log "[resolve-gradient] Resolving" gradient-id "->" href-id
"base-gradient tag:" (:tag base-gradient)
"base-gradient attrs:" (clj->js (keys (:attrs base-gradient)))
"base-gradient content:" (count (:content base-gradient)))
:clj nil)
(resolve-gradient href-id base-gradient defs (conj visited gradient-id))))]
(if resolved-base
;; Merge: base gradient attributes + referencing gradient attributes
;; Use referencing gradient's stops if present, otherwise use base stops
(let [base-attrs (:attrs resolved-base)
ref-attrs (:attrs gradient-node)
;; Start with base attributes (without id), then merge with ref attributes
;; This ensures ref attributes override base ones
base-attrs-clean (dissoc base-attrs :id)
ref-attrs-clean (dissoc ref-attrs :href :xlink:href :id)
;; Special handling for gradientTransform: if both have it, combine them
base-transform (get base-attrs :gradientTransform)
ref-transform (get ref-attrs :gradientTransform)
combined-transform (cond
(and base-transform ref-transform)
(str base-transform " " ref-transform) ;; Apply base first, then ref
base-transform base-transform
ref-transform ref-transform
:else nil)
;; Merge attributes: base first, then ref (ref overrides)
merged-attrs (-> (d/deep-merge base-attrs-clean ref-attrs-clean)
(cond-> combined-transform
(assoc :gradientTransform combined-transform)))
;; If referencing gradient has content (stops), use it; otherwise use base content
final-content (if (seq (:content gradient-node))
(:content gradient-node)
(:content resolved-base))
;; Process stops to extract stop-color and stop-opacity from style attributes
processed-content (process-gradient-stops final-content)
result {:tag (:tag gradient-node)
:attrs (assoc merged-attrs :id gradient-id)
:content processed-content}]
#?(:cljs (do
(js/console.log "[resolve-gradient] Merged gradient" gradient-id
"attrs:" (clj->js merged-attrs)
"x1:" (get merged-attrs :x1)
"y1:" (get merged-attrs :y1)
"x2:" (get merged-attrs :x2)
"y2:" (get merged-attrs :y2)
"gradientUnits:" (get merged-attrs :gradientUnits)
"gradientTransform:" (get merged-attrs :gradientTransform)
"content:" (count final-content) "stops")
;; Log each stop details
(doseq [[idx stop] (d/enumerate final-content)]
(let [stop-attrs (:attrs stop)
stop-style (get stop-attrs :style)
stop-color-val (get stop-attrs :stop-color)
stop-opacity-val (get stop-attrs :stop-opacity)]
(js/console.log (str "[resolve-gradient] Stop " idx ":")
"offset:" (get stop-attrs :offset)
"stop-color attr:" stop-color-val
"stop-color type:" (type stop-color-val)
"stop-opacity attr:" stop-opacity-val
"stop-opacity type:" (type stop-opacity-val)
"stop-opacity as number:" (when stop-opacity-val (d/parse-double stop-opacity-val 1))
"style:" stop-style
"parsed style:" (when (string? stop-style)
(let [parsed (reduce (fn [res item]
(let [[k v] (-> (str/trim item) (str/split ":" 2))
k (keyword k)]
(assoc res k v)))
{}
(str/split stop-style ";"))]
parsed))
"full attrs:" (clj->js stop-attrs)))))
:clj nil)
result)
(do
#?(:cljs (js/console.log "[resolve-gradient] No base gradient found for" gradient-id (when href-id (str "href=" href-id)))
:clj nil)
;; Process stops even for gradients without references to extract style attributes
(let [processed-content (process-gradient-stops (:content gradient-node))]
(assoc gradient-node :content processed-content)))))))]
(let [gradient-tags #{:linearGradient :radialGradient}
result (reduce-kv
(fn [acc id node]
(if (contains? gradient-tags (:tag node))
(do
#?(:cljs (js/console.log "[resolve-gradient-href] Processing gradient" id)
:clj nil)
(assoc acc id (resolve-gradient id node defs #{})))
(assoc acc id node)))
{}
defs)]
#?(:cljs (js/console.log "[resolve-gradient-href] Resolution complete. Processed" (count result) "defs")
:clj nil)
result)))
(defn create-svg-shapes
([svg-data pos objects frame-id parent-id selected center?]
(create-svg-shapes (uuid/next) svg-data pos objects frame-id parent-id selected center?))
@@ -112,6 +290,9 @@
(csvg/fix-percents)
(csvg/extract-defs))
;; Resolve gradient href references in all defs before processing shapes
def-nodes (resolve-gradient-href def-nodes)
;; In penpot groups have the size of their children. To
;; respect the imported svg size and empty space let's create
;; a transparent shape as background to respect the imported
@@ -142,12 +323,43 @@
(reduce (partial create-svg-children objects selected frame-id root-id svg-data)
[unames []]
(d/enumerate (->> (:content svg-data)
(mapv #(csvg/inherit-attributes root-attrs %)))))]
(mapv #(csvg/inherit-attributes root-attrs %)))))
[root-shape children])))
;; Collect all defs from children and merge into root shape
all-defs-from-children (reduce (fn [acc child]
(if-let [child-defs (:svg-defs child)]
(do
#?(:cljs (js/console.log "[create-svg-shapes] Found defs in child" (:name child) "defs:" (keys child-defs))
:clj nil)
(merge acc child-defs))
acc))
{}
children)
_ #?(:cljs (js/console.log "[create-svg-shapes] Root shape before defs merge:"
"root-shape name:" (:name root-shape)
"root-shape type:" (:type root-shape)
"def-nodes count:" (count def-nodes)
"def-nodes keys:" (keys def-nodes)
"all-defs-from-children count:" (count all-defs-from-children)
"all-defs-from-children keys:" (keys all-defs-from-children)
"children count:" (count children))
:clj nil)
;; Merge defs from svg-data and children into root shape
root-shape-with-defs (assoc root-shape :svg-defs (merge def-nodes all-defs-from-children))
_ #?(:cljs (js/console.log "[create-svg-shapes] Root shape after defs merge:"
"root-shape-with-defs name:" (:name root-shape-with-defs)
"root-shape-with-defs type:" (:type root-shape-with-defs)
"root-shape-with-defs svg-defs count:" (when (:svg-defs root-shape-with-defs) (count (:svg-defs root-shape-with-defs)))
"root-shape-with-defs svg-defs keys:" (when (:svg-defs root-shape-with-defs) (keys (:svg-defs root-shape-with-defs))))
:clj nil)]
[root-shape-with-defs children])))
(defn create-raw-svg
[name frame-id {:keys [x y width height offset-x offset-y]} {:keys [attrs] :as data}]
[name frame-id {:keys [x y width height offset-x offset-y defs] :as svg-data} {:keys [attrs] :as data}]
(let [props (csvg/attrs->props attrs)
vbox (grc/make-rect offset-x offset-y width height)]
(cts/setup-shape
@@ -160,10 +372,11 @@
:y y
:content data
:svg-attrs props
:svg-viewbox vbox})))
:svg-viewbox vbox
:svg-defs defs})))
(defn create-svg-root
[id frame-id parent-id {:keys [name x y width height offset-x offset-y attrs]}]
[id frame-id parent-id {:keys [name x y width height offset-x offset-y attrs defs] :as svg-data}]
(let [props (-> (dissoc attrs :viewBox :view-box :xmlns)
(d/without-keys csvg/inheritable-props)
(csvg/attrs->props))]
@@ -177,7 +390,8 @@
:height height
:x (+ x offset-x)
:y (+ y offset-y)
:svg-attrs props})))
:svg-attrs props
:svg-defs defs})))
(defn create-svg-children
[objects selected frame-id parent-id svg-data [unames children] [_index svg-element]]
@@ -198,7 +412,7 @@
(defn create-group
[name frame-id {:keys [x y width height offset-x offset-y] :as svg-data} {:keys [attrs]}]
[name frame-id {:keys [x y width height offset-x offset-y defs] :as svg-data} {:keys [attrs]}]
(let [transform (csvg/parse-transform (:transform attrs))
attrs (-> attrs
(d/without-keys csvg/inheritable-props)
@@ -214,7 +428,8 @@
:height height
:svg-transform transform
:svg-attrs attrs
:svg-viewbox vbox})))
:svg-viewbox vbox
:svg-defs defs})))
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
(when (and (contains? attrs :d) (seq (:d attrs)))
@@ -392,6 +607,17 @@
color-attr (if (= color-attr "currentColor") clr/black color-attr)
color-style (str/trim (dm/get-in shape [:svg-attrs :style :fill]))
color-style (if (= color-style "currentColor") clr/black color-style)]
#?(:cljs (when (or (str/includes? (str color-attr) "url(")
(str/includes? (str color-style) "url("))
(js/console.log "[setup-fill] Shape" (:name shape) "has gradient fill"
"fill-attr:" color-attr
"fill-style:" color-style
"svg-defs present?:" (some? (:svg-defs shape))
"svg-defs type:" (type (:svg-defs shape))
"svg-defs count:" (when (:svg-defs shape) (count (:svg-defs shape)))
"svg-defs keys:" (when (:svg-defs shape) (keys (:svg-defs shape)))
"all shape keys:" (keys shape)))
:clj nil)
(cond-> shape
;; Color present as attribute
(clr/color-string? color-attr)
@@ -503,6 +729,86 @@
(-> (update-in [:svg-attrs :style] dissoc :mixBlendMode)
(assoc :blend-mode (-> (dm/get-in shape [:svg-attrs :style :mixBlendMode]) assert-valid-blend-mode)))))
(defn- parse-svg-filter-to-native
"Attempts to convert SVG filters to native Penpot filters (blur, shadow).
This is a basic implementation that can be extended for more complex cases.
Currently supports:
- feGaussianBlur -> blur (layer-blur)
- Simple drop shadow filters -> shadow (drop-shadow)
Returns the shape with converted filters applied, or the original shape if
conversion is not possible or not supported."
[shape]
(let [filter-attr (or (dm/get-in shape [:svg-attrs :filter])
(dm/get-in shape [:svg-attrs :style :filter]))
svg-defs (:svg-defs shape)]
(if (and filter-attr svg-defs)
(let [filter-ids (csvg/extract-ids filter-attr)
filter-def (some #(get svg-defs %) filter-ids)]
(if filter-def
(let [filter-content (:content filter-def)
;; Try to find feGaussianBlur for blur conversion
gaussian-blur (some #(when (= :feGaussianBlur (:tag %)) %) filter-content)
;; Try to find drop shadow elements
drop-shadow-elements (filter (fn [elem]
(contains? #{:feOffset :feGaussianBlur :feColorMatrix} (:tag elem)))
filter-content)]
(let [offset-elem (some #(when (= :feOffset (:tag %)) %) filter-content)
;; Only create shadow if there's an feOffset (drop shadow requires offset)
;; If there's only feGaussianBlur without feOffset, it's just a blur
shape-with-blur
(if (and gaussian-blur (not (some? (:blur shape))))
(assoc shape :blur {:id (uuid/next)
:type :layer-blur
:value (-> (dm/get-in gaussian-blur [:attrs :stdDeviation])
(d/parse-double 0)) ;; For layer-blur, value = stdDeviation directly
:hidden false})
shape)
shape-with-shadow
(if (and offset-elem
(seq drop-shadow-elements)
(not (seq (:shadow shape-with-blur))))
(let [blur-elem (some #(when (= :feGaussianBlur (:tag %)) %) drop-shadow-elements)
dx (-> (dm/get-in offset-elem [:attrs :dx])
(d/parse-double 0))
dy (-> (dm/get-in offset-elem [:attrs :dy])
(d/parse-double 0))
blur-value (if blur-elem
(-> (dm/get-in blur-elem [:attrs :stdDeviation])
(d/parse-double 0)
(* 2))
0)
;; Default color - TODO: parse color from feColorMatrix
shadow-color "#000000"]
(assoc shape-with-blur :shadow [{:id (uuid/next)
:style :drop-shadow
:offset-x dx
:offset-y dy
:blur blur-value
:spread 0
:hidden false
:color {:color shadow-color :opacity 1}}]))
shape-with-blur)
;; Remove filter attribute if we successfully converted it
converted? (or (some? (:blur shape-with-shadow))
(seq (:shadow shape-with-shadow)))
final-shape (if converted?
(-> shape-with-shadow
(update :svg-attrs dissoc :filter)
(update-in [:svg-attrs :style] #(when % (dissoc % :filter))))
shape-with-shadow)]
final-shape))
shape))
shape)))
(defn setup-other [shape]
(cond-> shape
(= (dm/get-in shape [:svg-attrs :display]) "none")
@@ -534,7 +840,24 @@
(let [name (or (:id attrs) (tag->name tag))
att-refs (csvg/find-attr-references attrs)
defs (get svg-data :defs)
references (csvg/find-def-references defs att-refs)
;; Filter out false positive references:
;; 1. Colors in style attributes (hex colors like #f9dd67)
;; 2. Style fragments that contain CSS keywords (like stop-opacity)
;; 3. References that don't exist in defs
is-style-fragment? (fn [ref-id]
(or (clr/hex-color-string? (str "#" ref-id))
(str/includes? ref-id ";") ;; Contains CSS separator
(str/includes? ref-id "stop-opacity") ;; CSS keyword
(str/includes? ref-id "stop-color"))) ;; CSS keyword
valid-refs (->> att-refs
(remove is-style-fragment?) ;; Filter style fragments and hex colors
(filter #(contains? defs %))) ;; Only existing defs
all-refs (csvg/find-def-references defs valid-refs)
;; Filter the final result to ensure all references are valid defs
;; This prevents false positives from style attributes in gradient stops
references (->> all-refs
(remove is-style-fragment?) ;; Filter style fragments and hex colors
(filter #(contains? defs %))) ;; Only existing defs
href-id (or (:href attrs) (:xlink:href attrs) " ")
href-id (if (and (string? href-id)
@@ -574,19 +897,65 @@
#_other (create-raw-svg name frame-id svg-data element))]
(when (some? shape)
[(-> shape
(assoc :svg-defs (select-keys defs references))
(setup-fill)
(setup-stroke)
(setup-opacity)
(setup-other)
(update :svg-attrs (fn [attrs]
(if (empty? (:style attrs))
(dissoc attrs :style)
attrs)))
(cond-> ^boolean hidden
(assoc :hidden true)))
(let [selected-defs (select-keys defs references)]
#?(:cljs (when (seq selected-defs)
(js/console.log "[parse-svg-element] Shape" name "has" (count selected-defs) "defs:" (clj->js (keys selected-defs))
"references:" (clj->js references))
(doseq [[def-id def-node] selected-defs]
(when (contains? #{:linearGradient :radialGradient} (:tag def-node))
(let [def-attrs (:attrs def-node)
stops (:content def-node)]
(js/console.log "[parse-svg-element] Gradient def" def-id
"x1:" (get def-attrs :x1)
"y1:" (get def-attrs :y1)
"x2:" (get def-attrs :x2)
"y2:" (get def-attrs :y2)
"gradientUnits:" (get def-attrs :gradientUnits)
"gradientTransform:" (get def-attrs :gradientTransform)
"content:" (count stops) "stops")
;; Log each stop in detail
(doseq [[idx stop] (d/enumerate stops)]
(let [stop-attrs (:attrs stop)
stop-style (get stop-attrs :style)
parsed-style (when (string? stop-style)
(reduce (fn [res item]
(let [[k v] (-> (str/trim item) (str/split ":" 2))
k (keyword k)]
(assoc res k v)))
{}
(str/split stop-style ";")))]
(js/console.log (str "[parse-svg-element] Gradient def " def-id " - Stop " idx ":")
"offset:" (get stop-attrs :offset)
"stop-color attr:" (get stop-attrs :stop-color)
"stop-opacity attr:" (get stop-attrs :stop-opacity)
"style string:" stop-style
"parsed style:" (clj->js parsed-style)
"stop-color from style:" (when parsed-style (:stop-color parsed-style))
"stop-opacity from style:" (when parsed-style (:stop-opacity parsed-style))
"full stop attrs:" (clj->js stop-attrs))))))))
:clj nil)
[(let [shape-with-defs (assoc shape :svg-defs (select-keys defs references))]
#?(:cljs (js/console.log "[parse-svg-element] After assoc defs, shape has" (count (:svg-defs shape-with-defs)) "defs:" (keys (:svg-defs shape-with-defs)))
:clj nil)
(-> shape-with-defs
(#?(:cljs (fn [shape]
(js/console.log "[parse-svg-element] Before setup-fill, shape has svg-defs:" (some? (:svg-defs shape))
"count:" (when (:svg-defs shape) (count (:svg-defs shape)))
"keys:" (when (:svg-defs shape) (keys (:svg-defs shape))))
shape)
:clj identity))
(setup-fill)
(setup-stroke)
(setup-opacity)
(parse-svg-filter-to-native)
(setup-other)
(update :svg-attrs (fn [attrs]
(if (empty? (:style attrs))
(dissoc attrs :style)
attrs)))
(cond-> ^boolean hidden
(assoc :hidden true))))
(cond->> (:content element)
(contains? csvg/parent-tags tag)
(mapv (partial csvg/inherit-attributes attrs)))])))))
(mapv (partial csvg/inherit-attributes attrs)))]))))))

View File

@@ -14,6 +14,7 @@
[app.common.geom.shapes.bounds :as gsb]
[app.common.json :as json]
[app.common.svg :as csvg]
[app.util.object :as obj]
[rumext.v2 :as mf]))
(defn add-matrix [attrs transform-key transform-matrix]
@@ -86,7 +87,34 @@
[mf/Fragment #js {}])
props
(json/->js attrs :key-fn name)]
(json/->js attrs :key-fn name)
_ (when (or (= tag :stop) (contains? csvg/gradient-tags tag))
(js/console.log "[svg-node] Rendering node:"
"tag:" tag
"content count:" (count content)
"has stops?" (some #(= :stop (:tag %)) content)))
_ (when (= tag :stop)
(let [stop-color-attr (get attrs :stop-color)
stop-opacity-attr (get attrs :stop-opacity)
stop-opacity-type (type stop-opacity-attr)
stop-opacity-js (obj/get props "stopOpacity")
stop-opacity-js-type (type stop-opacity-js)]
(js/console.log "[svg-node] Rendering stop - DETAILED:"
"tag:" tag
"attrs before csvg/attrs->props:" (clj->js attrs)
"stop-color attr:" stop-color-attr
"stop-opacity attr:" stop-opacity-attr
"stop-opacity attr type:" stop-opacity-type
"stop-opacity as number:" (when stop-opacity-attr (d/parse-double stop-opacity-attr 1))
"props after json/->js:" props
"stopColor prop:" (obj/get props "stopColor")
"stopOpacity prop:" stop-opacity-js
"stopOpacity prop type:" stop-opacity-js-type
"offset prop:" (obj/get props "offset")
"all props keys:" (js/Object.keys props)
"all props:" props)))]
[:> (name tag) props
[:> wrapper wrapper-props
@@ -113,6 +141,34 @@
[{:keys [shape render-id]}]
(let [defs (:svg-defs shape)
_ (js/console.log "[svg-defs] Rendering defs for shape - DETAILED:"
"shape name:" (:name shape)
"shape type:" (:type shape)
"shape id:" (:id shape)
"shape keys:" (keys shape)
"has-svg-defs?:" (contains? shape :svg-defs)
"svg-defs value:" (:svg-defs shape)
"svg-defs type:" (when (:svg-defs shape) (type (:svg-defs shape)))
"svg-defs count:" (when defs (count defs))
"svg-defs keys:" (when defs (keys defs))
"svg-defs empty?:" (empty? defs)
"full shape:" (clj->js shape)
"defs details:" (when defs (clj->js (mapv (fn [[k v]]
{:key k
:tag (:tag v)
:attrs-keys (keys (:attrs v))
:content-count (count (:content v))
:content (clj->js (:content v))
:has-stops (some #(= :stop (:tag %)) (:content v))
:stops (when-let [stops (seq (filter #(= :stop (:tag %)) (:content v)))]
(clj->js (mapv (fn [stop]
{:tag (:tag stop)
:attrs (:attrs stop)
:stop-color (get-in stop [:attrs :stop-color])
:stop-opacity (get-in stop [:attrs :stop-opacity])})
stops)))})
defs))))
transform (mf/with-memo [shape]
(if (= :svg-raw (:type shape))
(gmt/matrix)

View File

@@ -8,8 +8,10 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.svg :as csvg]
[app.common.types.color :as clr]
[clojure.string :as str]))
@@ -81,9 +83,45 @@
width (max 0.01 (dm/get-prop rect :width))
height (max 0.01 (dm/get-prop rect :height))
origin-x (or (dm/get-prop rect :x) (dm/get-prop rect :x1) 0)
origin-y (or (dm/get-prop rect :y) (dm/get-prop rect :y1) 0)]
(gpt/point (/ (- (dm/get-prop pt :x) origin-x) width)
(/ (- (dm/get-prop pt :y) origin-y) height)))
origin-y (or (dm/get-prop rect :y) (dm/get-prop rect :y1) 0)
viewbox (:svg-viewbox shape)]
(js/console.log "[normalize-point] Normalizing point with userSpaceOnUse:"
"pt:" (clj->js pt)
"shape selrect:" (clj->js rect)
"shape svg-viewbox:" (clj->js viewbox)
"shape type:" (dm/get-prop shape :type)
"shape transform:" (dm/get-prop shape :transform))
(let [;; For userSpaceOnUse, coordinates are in SVG user space
;; We need to transform them to shape space before normalizing
transformed-pt (if viewbox
(let [{svg-x :x svg-y :y svg-width :width svg-height :height} viewbox
scale-x (/ width svg-width)
scale-y (/ height svg-height)
;; Transform from viewBox space to selrect space
transformed-x (+ origin-x (* (- (dm/get-prop pt :x) svg-x) scale-x))
transformed-y (+ origin-y (* (- (dm/get-prop pt :y) svg-y) scale-y))
transformed (gpt/point transformed-x transformed-y)]
(js/console.log "[normalize-point] After viewBox transform:"
"svg-x:" svg-x "svg-y:" svg-y
"svg-width:" svg-width "svg-height:" svg-height
"scale-x:" scale-x "scale-y:" scale-y
"transformed:" (clj->js transformed))
;; Apply shape transform if needed
(if-let [transform-matrix (and (or (= :path (dm/get-prop shape :type))
(= :group (dm/get-prop shape :type)))
(gsh/transform-matrix shape))]
(let [final-pt (gpt/transform transformed transform-matrix)]
(js/console.log "[normalize-point] After shape transform:"
"final:" (clj->js final-pt))
final-pt)
transformed))
pt)
normalized-x (/ (- (dm/get-prop transformed-pt :x) origin-x) width)
normalized-y (/ (- (dm/get-prop transformed-pt :y) origin-y) height)]
(js/console.log "[normalize-point] Final normalized point:"
"normalized-x:" normalized-x
"normalized-y:" normalized-y)
(gpt/point normalized-x normalized-y)))
pt))
(defn- normalize-attrs
@@ -143,6 +181,15 @@
(defn- resolve-gradient-node
[shape gradient-id]
(let [defs (dm/get-prop shape :svg-defs)]
(js/console.log "[resolve-gradient-node] Resolving gradient:"
"gradient-id:" gradient-id
"shape name:" (dm/get-prop shape :name)
"shape type:" (dm/get-prop shape :type)
"shape id:" (dm/get-prop shape :id)
"has svg-defs?:" (some? defs)
"svg-defs count:" (when defs (count defs))
"svg-defs keys:" (when defs (keys defs))
"svg-defs:" (when defs defs))
(when (and defs gradient-id)
(let [chain (loop [gid gradient-id
seen #{}
@@ -204,6 +251,18 @@
(get style :stop-opacity)
(get style :stopOpacity))
offset (or (get attrs :offset) "0")]
(js/console.log "[parse-gradient-stop] Parsing stop:"
"stop-node attrs:" (clj->js attrs)
"normalized attrs:" (clj->js (normalize-attrs (:attrs stop-node)))
"style:" style
"stop-color attr:" (get attrs :stop-color)
"stop-opacity attr:" (get attrs :stop-opacity)
"stop-opacity from style:" (get style :stop-opacity)
"stop-opacity from style (stopOpacity):" (get style :stopOpacity)
"final opacity:" (some-> opacity parse-opacity)
"color-value:" color-value
"color:" color
"offset:" offset)
(when color
(d/without-nils {:color color
:opacity (some-> opacity parse-opacity)
@@ -230,11 +289,24 @@
(when (= (keyword (:tag node)) :stop)
(parse-gradient-stop node))))
vec)]
(js/console.log "[build-linear-gradient] Building gradient:"
"units:" units
"x1:" x1 "y1:" y1 "x2:" x2 "y2:" y2
"transform:" transform
"shape selrect:" (shape->selrect shape)
"stops count:" (count stops))
(when (seq stops)
(let [points (apply-gradient-transform [(gpt/point x1 y1)
(gpt/point x2 y2)]
transform)
[start end] (map #(normalize-point % units shape) points)]
[start end] (map #(normalize-point % units shape) points)
selrect (shape->selrect shape)]
(js/console.log "[build-linear-gradient] After normalization:"
"original points:" (clj->js points)
"normalized start:" (clj->js start)
"normalized end:" (clj->js end)
"selrect:" (clj->js selrect)
"units:" units)
{:type :linear
:start-x (dm/get-prop start :x)
:start-y (dm/get-prop start :y)
@@ -278,14 +350,30 @@
(string? trimmed) trimmed
(some? trimmed) (str trimmed)
:else nil)]
(js/console.log "[svg-gradient->fill] Converting gradient fill:"
"value:" value
"trimmed:" trimmed
"fill-str:" fill-str
"shape name:" (dm/get-prop shape :name)
"shape type:" (dm/get-prop shape :type))
(when-let [gradient-id
(when (string? fill-str)
(some-> (re-matches url-fill-pattern fill-str)
(nth 1 nil)))]
(js/console.log "[svg-gradient->fill] Extracted gradient-id:" gradient-id)
(when-let [node (resolve-gradient-node shape gradient-id)]
(js/console.log "[svg-gradient->fill] Resolved gradient node:"
"tag:" (:tag node)
"has content:" (seq (:content node))
"content count:" (count (:content node))
"node:" (clj->js node))
(case (:tag node)
:linearGradient (build-linear-gradient shape node)
:radialGradient (build-radial-gradient shape node)
:linearGradient (let [result (build-linear-gradient shape node)]
(js/console.log "[svg-gradient->fill] Built linear gradient:" (clj->js result))
result)
:radialGradient (let [result (build-radial-gradient shape node)]
(js/console.log "[svg-gradient->fill] Built radial gradient:" (clj->js result))
result)
nil)))))
(defn- parse-svg-fill
@@ -333,6 +421,19 @@
(let [base-fills (dm/get-prop shape :fills)
fallback (svg-fill->fills shape)
type (dm/get-prop shape :type)]
(js/console.log "[resolve-shape-fills] Resolving fills for shape:"
"shape name:" (dm/get-prop shape :name)
"shape type:" type
"shape id:" (dm/get-prop shape :id)
"has base-fills?:" (seq base-fills)
"base-fills:" (clj->js base-fills)
"has fallback?:" (seq fallback)
"fallback:" (clj->js fallback)
"has svg-attrs?:" (contains? shape :svg-attrs)
"svg-attrs fill:" (dm/get-in shape [:svg-attrs :fill])
"svg-attrs style fill:" (dm/get-in shape [:svg-attrs :style :fill])
"has svg-defs?:" (contains? shape :svg-defs)
"svg-defs:" (when (contains? shape :svg-defs) (clj->js (:svg-defs shape))))
(cond
(seq base-fills) base-fills
(seq fallback) fallback