@solidrt/2d 0.0.68 → 0.0.70

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.
package/AGENTS.md CHANGED
@@ -603,10 +603,10 @@ for a zoom animation that would re-rasterize every label per frame.
603
603
  Layout is `layoutText` (text-layout.ts, pure): `\n` breaks, `maxWidth`
604
604
  wraps with core's greedy rule, `align` per line, `anchor`
605
605
  (start/middle/end, as d-text) and `anchorY` (top/middle/baseline/bottom)
606
- place the run's point, `letterSpacing` and `lineHeight` as named. The
607
- run reports `width`, `height`, `ascent` and `lines`. A run's `fontSize`
608
- defaults to the face's; glyphs scale their cells by `fontSize /
609
- font.size`.
606
+ place the run's point, `lineHeight` as named; `letterSpacing` is shaped
607
+ into the units by the engine. The run reports `width`, `height`, `ascent`
608
+ and `lines`. A run's `fontSize` defaults to the face's; glyphs scale their
609
+ cells by `fontSize / font.size`.
610
610
 
611
611
  ### Pure pieces
612
612
 
@@ -720,8 +720,16 @@ sits, `viewport` is the nearest view (the `<SpriteLayer>`'s own, or
720
720
  inside a `<View2d>` that view), what a `<Camera2d>` drives, and
721
721
  `pointer` that view's feed (the owner's `pointer` prop, null without
722
722
  one); read under `output={false}` outside a `<View2d>` `viewport` throws. A FILL-mode
723
- view is 1x1 until the first layout: `ref` fires at mount, before it, so
724
- `viewport.size()` reads 1x1 there. Sprites added and positioned from
723
+ leaf is size-contained (`contain="size"`, the CSS layout prop every
724
+ element takes): it adds nothing to its ancestors' sizes, exactly as an
725
+ empty `<view>` at 100% would, so a column of header, layer pane
726
+ (`flexGrow: 1`) and footer needs no `minHeight={0}`, and a pane with no
727
+ definite height does not come out square. The leaf then has no size of
728
+ its own at all: a parent with no size leaves it empty, the view is
729
+ invisible, and the component warns once with the stack of the component
730
+ that built it, naming the fix (a size, or flexGrow in a sized column). A
731
+ FILL-mode view is 1x1 until the first layout: `ref` fires at mount,
732
+ before it, so `viewport.size()` reads 1x1 there. Sprites added and positioned from
725
733
  `ref` (the pool, the opening scene) are fine - the first layout resizes
726
734
  the view before the first paint - but anything that needs the real size
727
735
  (centering on the view) waits for `onLayout`, or works in a `designSize`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/2d",
3
- "version": "0.0.68",
3
+ "version": "0.0.70",
4
4
  "license": "MIT",
5
5
  "funding": "https://github.com/sponsors/wellawaretech",
6
6
  "author": "Antoine van Wel",
@@ -15,10 +15,10 @@
15
15
  "AGENTS.md"
16
16
  ],
17
17
  "devDependencies": {
18
- "@solidrt/test": "0.0.68"
18
+ "@solidrt/test": "0.0.70"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "@solidjs/signals": "2.0.0-rc.13",
22
- "@solidrt/core": "0.0.68"
22
+ "@solidrt/core": "0.0.70"
23
23
  }
24
24
  }
@@ -1,4 +1,4 @@
1
- import { createEffect, displayScale, getBoundingBoxViewport, getLayoutBox, onCleanup, onLayout, untrack } from "@solidrt/core"
1
+ import { createEffect, displayScale, getBoundingBoxViewport, getLayoutBox, onCleanup, onLayout, untrack, warnOnce } from "@solidrt/core"
2
2
  import type { Element, ParentComponent, PointerFeed, TextureId } from "@solidrt/core"
3
3
  import type { BlendMode } from "@solidrt/core/gpu"
4
4
  import type { Atlas } from "../atlas.ts"
@@ -314,7 +314,18 @@ export let SpriteLayer: ParentComponent<SpriteLayerProps> = props => {
314
314
  // one onLayout applies size then oversample in order; setSize no-ops
315
315
  // when nothing changed.
316
316
  if (fill) {
317
+ let warnEmpty = warnOnce()
317
318
  onLayout(() => {
319
+ // The leaf adds no size of its own (contain="size" below), so a
320
+ // parent with no size leaves it empty: nothing renders, and nothing
321
+ // on screen says why.
322
+ let box = leaf && getLayoutBox(leaf)
323
+ if (box && (box.width <= 0 || box.height <= 0)) {
324
+ warnEmpty(
325
+ `SpriteLayer: the fill leaf laid out ${box.width} x ${box.height}, so the view is invisible. ` +
326
+ `It adds no size of its own: give its parent a size (width/height, or flexGrow in a sized column), as on the web.`,
327
+ )
328
+ }
318
329
  let size = viewSize()
319
330
  if (size) view.setSize(size.width, size.height)
320
331
  })
@@ -354,6 +365,7 @@ export let SpriteLayer: ParentComponent<SpriteLayerProps> = props => {
354
365
  src={view.texture}
355
366
  width={fill ? "100%" : props.width}
356
367
  height={fill ? "100%" : props.height}
368
+ contain={fill ? "size" : undefined}
357
369
  onPointerDown={events ? viewHandlers.onPointerDown : undefined}
358
370
  onPointerMove={events ? viewHandlers.onPointerMove : undefined}
359
371
  onPointerUp={events ? viewHandlers.onPointerUp : undefined}
package/src/font.ts CHANGED
@@ -14,9 +14,8 @@
14
14
  // (`Atlas.sdf`), and "mask", coverage at the face's exact size for drawing
15
15
  // 1:1.
16
16
  import { getOwner, onCleanup } from "@solidrt/core"
17
- import type { PreparedText } from "@solidrt/core"
18
17
  import { createFont, destroyFont, fontAtlas, glyphCells, prepareText, requestGlyphs } from "flux:font"
19
- import type { FontAtlasOptions, FontFace, FontId, FontPrepareOptions, GlyphCell } from "flux:font"
18
+ import type { FontAtlasOptions, FontFace, FontId, FontPreparedText, FontPrepareOptions, GlyphCell } from "flux:font"
20
19
  import type { Atlas } from "./atlas.ts"
21
20
  import type { Frame } from "./frames.ts"
22
21
 
@@ -68,7 +67,7 @@ export type SpriteFont = {
68
67
  readonly fontSize: number
69
68
  /** Shape `text` on the engine (see flux:font prepareText): every unit
70
69
  * carries its glyphs, for `layoutText`. */
71
- prepare(text: string, options?: FontPrepareOptions): PreparedText
70
+ prepare(text: string, options?: FontPrepareOptions): FontPreparedText
72
71
  /** The glyph's frame and placement, or null while its cell is not in
73
72
  * the atlas (request it with `request`). */
74
73
  glyph(id: number): Glyph | null
@@ -6,8 +6,9 @@
6
6
  // unit placed whole), restated here rather than imported because core
7
7
  // loads the gui modules and this file is checked headless on the flux
8
8
  // binary (tests/text-layout.test.ts), like frames.ts. No GPU or GUI
9
- // imports: the PreparedText import is a type.
10
- import type { PreparedText, TextUnit } from "@solidrt/core"
9
+ // imports: the flux:font import is a type. Letter spacing is the
10
+ // engine's, shaped into the units (`prepareText`'s `letterSpacing`).
11
+ import type { FontPreparedText, FontTextUnit } from "flux:font"
11
12
 
12
13
  /** The anchor of the run's x: where the point sits on each line's
13
14
  * extent (SVG's text-anchor, d-text's `anchor`). */
@@ -28,8 +29,6 @@ export type TextLayoutOptions = {
28
29
  anchor?: TextAnchor
29
30
  /** Default "top". */
30
31
  anchorY?: TextAnchorY
31
- /** Added after every glyph, run pixels (CSS letter-spacing); default 0. */
32
- letterSpacing?: number
33
32
  /** Baseline to baseline, run pixels; default each line's own ascent
34
33
  * plus descent. */
35
34
  lineHeight?: number
@@ -65,24 +64,18 @@ export type TextLayout = {
65
64
 
66
65
  /**
67
66
  * Lay out a prepared text (from `SpriteFont.prepare`) into glyph
68
- * placements. Throws on units without glyphs (a text prepared on
69
- * Impeller, which exposes none).
67
+ * placements.
70
68
  */
71
- export function layoutText(prepared: PreparedText, opts?: TextLayoutOptions): TextLayout {
69
+ export function layoutText(prepared: FontPreparedText, opts?: TextLayoutOptions): TextLayout {
72
70
  let units = prepared.units
73
71
  let maxWidth = opts?.maxWidth ?? Infinity
74
72
  if (!(maxWidth > 0)) throw new Error(`layoutText: maxWidth must be positive, got ${maxWidth}`)
75
- let letterSpacing = opts?.letterSpacing ?? 0
76
- if (!Number.isFinite(letterSpacing)) throw new Error(`layoutText: letterSpacing must be finite, got ${letterSpacing}`)
77
- for (let i = 0; i < units.length; i++) {
78
- if (units[i]!.glyphs === undefined) throw new Error(`layoutText: unit ${i} ("${units[i]!.text}") carries no glyphs; prepare the text on a sprite font`)
79
- }
80
73
  // Break into lines: the greedy pass, lines as unit ranges.
81
74
  type Range = { from: number; to: number; ascent: number; descent: number; width: number }
82
75
  let ranges: Range[] = []
83
76
  let cursor = 0
84
77
  while (cursor < units.length) {
85
- ranges.push(nextLine(units, cursor, maxWidth, letterSpacing))
78
+ ranges.push(nextLine(units, cursor, maxWidth))
86
79
  cursor = ranges[ranges.length - 1]!.to
87
80
  }
88
81
  // A text with no units (empty) still has one empty line of no height.
@@ -115,25 +108,26 @@ export function layoutText(prepared: PreparedText, opts?: TextLayoutOptions): Te
115
108
  let pen = line.x
116
109
  for (let i = line.from; i < line.to; i++) {
117
110
  let unit = units[i]!
118
- for (let g of unit.glyphs!) {
119
- glyphs.push({ id: g.id, x: pen + g.x, y: line.y + g.y })
120
- pen += letterSpacing
121
- }
111
+ for (let g of unit.glyphs) glyphs.push({ id: g.id, x: pen + g.x, y: line.y + g.y })
122
112
  pen += unit.advance
123
113
  }
124
114
  }
125
115
  return { glyphs, lines, width: runWidth, height, ascent: firstAscent }
126
116
  }
127
117
 
118
+ // Slack a line's width may be exceeded by and still fit, logical px: core's
119
+ // FIT_SLACK, the same value, for the same reason (a width that came back
120
+ // from a layout box is a float ulp off the width the text measured).
121
+ const FIT_SLACK = 1 / 64
122
+
128
123
  // Ink width of the wrap unit starting at `index` (core's unitInk): its
129
- // advance through every glued piece after it, plus the last piece's ink,
130
- // with the letter spacing its glyphs add.
131
- function unitInk(units: TextUnit[], index: number, letterSpacing: number): number {
132
- let ink = units[index]!.width + letterSpacing * units[index]!.glyphs!.length
124
+ // advance through every glued piece after it, plus the last piece's ink.
125
+ function unitInk(units: FontTextUnit[], index: number): number {
126
+ let ink = units[index]!.width
133
127
  let advance = 0
134
128
  for (let j = index + 1; j < units.length && units[j]!.glue; j++) {
135
- advance += units[j - 1]!.advance + letterSpacing * units[j - 1]!.glyphs!.length
136
- ink = advance + units[j]!.width + letterSpacing * units[j]!.glyphs!.length
129
+ advance += units[j - 1]!.advance
130
+ ink = advance + units[j]!.width
137
131
  }
138
132
  return ink
139
133
  }
@@ -142,15 +136,15 @@ function unitInk(units: TextUnit[], index: number, letterSpacing: number): numbe
142
136
  // layoutNextLine): units go on while the pen plus the unit's ink stays
143
137
  // within the width; a hard break ends the line; a unit wider than the
144
138
  // line on its own goes on whole.
145
- function nextLine(units: TextUnit[], cursor: number, width: number, letterSpacing: number) {
139
+ function nextLine(units: FontTextUnit[], cursor: number, width: number) {
146
140
  let pen = 0
147
141
  let ascent = 0
148
142
  let descent = 0
149
143
  let i = cursor
150
144
  while (i < units.length) {
151
145
  let unit = units[i]!
152
- if (i > cursor && !unit.glue && pen + unitInk(units, i, letterSpacing) > width) break
153
- pen += unit.advance + letterSpacing * unit.glyphs!.length
146
+ if (i > cursor && !unit.glue && pen + unitInk(units, i) > width + FIT_SLACK) break
147
+ pen += unit.advance
154
148
  if (unit.ascent > ascent) ascent = unit.ascent
155
149
  if (unit.descent > descent) descent = unit.descent
156
150
  i++
@@ -158,6 +152,6 @@ function nextLine(units: TextUnit[], cursor: number, width: number, letterSpacin
158
152
  }
159
153
  let last = units[i - 1]!
160
154
  // The ink width: trailing whitespace off, like the unit's own `width`.
161
- let lineWidth = pen - (last.advance + letterSpacing * last.glyphs!.length) + last.width + letterSpacing * last.glyphs!.length
155
+ let lineWidth = pen - last.advance + last.width
162
156
  return { from: cursor, to: i, ascent, descent, width: Math.max(lineWidth, 0) }
163
157
  }
package/src/text.ts CHANGED
@@ -23,7 +23,7 @@ export type TextStyle = {
23
23
  fontSize?: number
24
24
  /** Baseline to baseline in layer pixels; default each line's own box. */
25
25
  lineHeight?: number
26
- /** Added after every glyph, layer pixels; default 0. */
26
+ /** Extra advance after every character, layer pixels (CSS letter-spacing); default the font's. */
27
27
  letterSpacing?: number
28
28
  /** Wrap at this width; unset, only `\n` breaks. */
29
29
  maxWidth?: number
@@ -149,13 +149,12 @@ function relayout(run: RunState, style: TextStyle): void {
149
149
  run._style = style
150
150
  run.text = style.text
151
151
  let fontSize = style.fontSize ?? run.font.fontSize
152
- let prepared = run.font.prepare(style.text, { fontSize, lineHeight: 0 })
152
+ let prepared = run.font.prepare(style.text, { fontSize, lineHeight: 0, letterSpacing: style.letterSpacing })
153
153
  run._layout = layoutText(prepared, {
154
154
  maxWidth: style.maxWidth,
155
155
  align: style.align,
156
156
  anchor: style.anchor,
157
157
  anchorY: style.anchorY,
158
- letterSpacing: style.letterSpacing,
159
158
  lineHeight: style.lineHeight,
160
159
  })
161
160
  run.width = run._layout.width