lgimgui 0.1.0
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/README.md +111 -0
- package/demo/public/materials.json +809 -0
- package/package.json +39 -0
- package/src/core/curve.js +71 -0
- package/src/core/frame.js +272 -0
- package/src/core/geometry.js +61 -0
- package/src/core/ids.js +31 -0
- package/src/core/input.js +294 -0
- package/src/core/layout.js +220 -0
- package/src/core/spring.js +113 -0
- package/src/core/state.js +54 -0
- package/src/core/theme.js +77 -0
- package/src/index.js +221 -0
- package/src/material/adapt.js +72 -0
- package/src/material/material.js +131 -0
- package/src/material/optics.js +49 -0
- package/src/material/params.js +83 -0
- package/src/material/presets.js +291 -0
- package/src/render/batch.js +236 -0
- package/src/render/context.js +138 -0
- package/src/render/glyphs.js +177 -0
- package/src/render/probe.js +64 -0
- package/src/render/pyramid.js +48 -0
- package/src/render/renderer.js +296 -0
- package/src/render/shaders/color.js +58 -0
- package/src/render/shaders/glass.js +241 -0
- package/src/render/shaders/ui.js +194 -0
- package/src/widgets/composite.js +355 -0
- package/src/widgets/controls.js +545 -0
- package/src/widgets/desktop.js +144 -0
- package/src/widgets/glass.js +326 -0
- package/src/widgets/primitives.js +355 -0
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lgimgui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "liquid glass immediate mode gui for the browser",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./src/*": "./src/*",
|
|
10
|
+
"./materials.json": "./demo/public/materials.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"demo/public/materials.json"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "vite demo --host",
|
|
18
|
+
"build": "vite build demo",
|
|
19
|
+
"preview": "vite preview demo",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=24"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"vite": "^6.0.0",
|
|
28
|
+
"vitest": "^3.0.0"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/nvms/lgimgui.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/nvms/lgimgui/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/nvms/lgimgui#readme"
|
|
39
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
function cubicAt(p0, p1, p2, p3, t) {
|
|
2
|
+
const u = 1 - t
|
|
3
|
+
const a = u * u * u
|
|
4
|
+
const b = 3 * u * u * t
|
|
5
|
+
const c = 3 * u * t * t
|
|
6
|
+
const d = t * t * t
|
|
7
|
+
return { x: a * p0.x + b * p1.x + c * p2.x + d * p3.x, y: a * p0.y + b * p1.y + c * p2.y + d * p3.y }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function cubicPoints({ from, c1, c2, to }, options = {}) {
|
|
11
|
+
const chord = Math.hypot(to.x - from.x, to.y - from.y) + Math.hypot(c1.x - from.x, c1.y - from.y) + Math.hypot(c2.x - to.x, c2.y - to.y)
|
|
12
|
+
const segments = options.segments ?? Math.max(8, Math.min(96, Math.ceil(chord / (options.step ?? 10))))
|
|
13
|
+
const points = []
|
|
14
|
+
for (let i = 0; i <= segments; i++) points.push(cubicAt(from, c1, c2, to, i / segments))
|
|
15
|
+
return points
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function horizontalCubic(from, to, options = {}) {
|
|
19
|
+
const distance = Math.abs(to.x - from.x)
|
|
20
|
+
const reach = Math.max(options.minReach ?? 40, distance * (options.tension ?? 0.5))
|
|
21
|
+
return { from, c1: { x: from.x + reach, y: from.y }, c2: { x: to.x - reach, y: to.y }, to }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function segmentQuad(a, b, width) {
|
|
25
|
+
const dx = b.x - a.x
|
|
26
|
+
const dy = b.y - a.y
|
|
27
|
+
const length = Math.hypot(dx, dy)
|
|
28
|
+
const cx = (a.x + b.x) / 2
|
|
29
|
+
const cy = (a.y + b.y) / 2
|
|
30
|
+
return {
|
|
31
|
+
rect: { x: cx - (length + width) / 2, y: cy - width / 2, w: length + width, h: width },
|
|
32
|
+
rotation: Math.atan2(dy, dx),
|
|
33
|
+
radius: width / 2,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function walkDashes(points, dash, gap) {
|
|
38
|
+
const quads = []
|
|
39
|
+
const period = dash + gap
|
|
40
|
+
let travelled = 0
|
|
41
|
+
for (let i = 1; i < points.length; i++) {
|
|
42
|
+
const a = points[i - 1]
|
|
43
|
+
const b = points[i]
|
|
44
|
+
const length = Math.hypot(b.x - a.x, b.y - a.y)
|
|
45
|
+
if (length === 0) continue
|
|
46
|
+
let local = 0
|
|
47
|
+
while (local < length) {
|
|
48
|
+
const phase = (travelled + local) % period
|
|
49
|
+
const onDash = phase < dash
|
|
50
|
+
const remaining = onDash ? dash - phase : period - phase
|
|
51
|
+
const end = Math.min(length, local + remaining)
|
|
52
|
+
if (onDash) {
|
|
53
|
+
const t0 = local / length
|
|
54
|
+
const t1 = end / length
|
|
55
|
+
quads.push({ a: { x: a.x + (b.x - a.x) * t0, y: a.y + (b.y - a.y) * t0 }, b: { x: a.x + (b.x - a.x) * t1, y: a.y + (b.y - a.y) * t1 } })
|
|
56
|
+
}
|
|
57
|
+
local = end
|
|
58
|
+
}
|
|
59
|
+
travelled += length
|
|
60
|
+
}
|
|
61
|
+
return quads
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function strokeQuads(points, options = {}) {
|
|
65
|
+
const width = options.width ?? 2
|
|
66
|
+
if (points.length < 2) return []
|
|
67
|
+
if (options.dash > 0) return walkDashes(points, options.dash, options.gap ?? options.dash).map(({ a, b }) => segmentQuad(a, b, width))
|
|
68
|
+
const quads = []
|
|
69
|
+
for (let i = 1; i < points.length; i++) quads.push(segmentQuad(points[i - 1], points[i], width))
|
|
70
|
+
return quads
|
|
71
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { layout, measure, position, scaleNode, translateNode } from './layout.js'
|
|
2
|
+
import { CONTENT_THEME } from './theme.js'
|
|
3
|
+
import { rectIntersect } from './geometry.js'
|
|
4
|
+
|
|
5
|
+
export { scaleNode, translateNode }
|
|
6
|
+
|
|
7
|
+
function rectsOverlap(a, b) {
|
|
8
|
+
return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createNode(kind, props, extra) {
|
|
12
|
+
return {
|
|
13
|
+
kind,
|
|
14
|
+
id: props.id ?? null,
|
|
15
|
+
props,
|
|
16
|
+
children: [],
|
|
17
|
+
parent: null,
|
|
18
|
+
natural: { w: 0, h: 0 },
|
|
19
|
+
content: { w: 0, h: 0 },
|
|
20
|
+
rect: { x: 0, y: 0, w: 0, h: 0 },
|
|
21
|
+
intrinsic: null,
|
|
22
|
+
paint: null,
|
|
23
|
+
layer: 0,
|
|
24
|
+
...extra,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createFrameDriver({ renderer, input, state, ids, clock, probe }) {
|
|
29
|
+
let root = null
|
|
30
|
+
let stack = []
|
|
31
|
+
let popups = []
|
|
32
|
+
let viewport = { w: 0, h: 0 }
|
|
33
|
+
let frame = null
|
|
34
|
+
let focusOrder = []
|
|
35
|
+
let ghosts = new Map()
|
|
36
|
+
let lastStrata = []
|
|
37
|
+
|
|
38
|
+
function begin(width, height, seconds) {
|
|
39
|
+
viewport = { w: width, h: height }
|
|
40
|
+
ids.reset()
|
|
41
|
+
clock.tick(seconds)
|
|
42
|
+
frame = input.beginFrame()
|
|
43
|
+
root = createNode('box', { direction: 'stack', align: 'stretch', pad: 0 })
|
|
44
|
+
stack = [root]
|
|
45
|
+
popups = []
|
|
46
|
+
focusOrder = []
|
|
47
|
+
return frame
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function current() {
|
|
51
|
+
return stack[stack.length - 1]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function add(node) {
|
|
55
|
+
const parent = current()
|
|
56
|
+
node.parent = parent
|
|
57
|
+
node.layer = node.props.detached ? node.layer : parent.layer
|
|
58
|
+
parent.children.push(node)
|
|
59
|
+
return node
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function open(node) {
|
|
63
|
+
add(node)
|
|
64
|
+
stack.push(node)
|
|
65
|
+
if (node.id) ids.push(node.props.key ?? node.props.label ?? node.kind)
|
|
66
|
+
return node
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function close() {
|
|
70
|
+
const node = stack.pop()
|
|
71
|
+
if (node.id) ids.pop()
|
|
72
|
+
return node
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function openPopup(node) {
|
|
76
|
+
node.props.detached = true
|
|
77
|
+
node.layer = current().layer + 1
|
|
78
|
+
node.parent = current()
|
|
79
|
+
popups.push(node)
|
|
80
|
+
stack.push(node)
|
|
81
|
+
if (node.id) ids.push(node.props.key ?? node.kind)
|
|
82
|
+
return node
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function registerFocusable(id) {
|
|
86
|
+
focusOrder.push(id)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function placePopup(node) {
|
|
90
|
+
measure(node, {})
|
|
91
|
+
const anchor = node.props.anchorNode?.rect ?? node.props.anchor ?? { x: 0, y: 0, w: 0, h: 0 }
|
|
92
|
+
const gap = node.props.gap ?? 6
|
|
93
|
+
let w = typeof node.props.w === 'number' ? node.props.w : Math.max(node.natural.w, node.props.minW ?? 0)
|
|
94
|
+
if (node.props.matchAnchor) w = Math.max(w, anchor.w)
|
|
95
|
+
const h = typeof node.props.h === 'number' ? node.props.h : node.natural.h
|
|
96
|
+
const placement = node.props.placement ?? 'below'
|
|
97
|
+
let x = anchor.x
|
|
98
|
+
let y = anchor.y + anchor.h + gap
|
|
99
|
+
if (placement === 'above') y = anchor.y - h - gap
|
|
100
|
+
if (placement === 'right') {
|
|
101
|
+
x = anchor.x + anchor.w + gap
|
|
102
|
+
y = anchor.y
|
|
103
|
+
}
|
|
104
|
+
if (placement === 'center') {
|
|
105
|
+
x = anchor.x + (anchor.w - w) / 2
|
|
106
|
+
y = anchor.y + (anchor.h - h) / 2
|
|
107
|
+
}
|
|
108
|
+
if (node.props.alignEnd) x = anchor.x + anchor.w - w
|
|
109
|
+
if (x + w > viewport.w - 8) x = viewport.w - 8 - w
|
|
110
|
+
if (y + h > viewport.h - 8) y = placement === 'below' ? anchor.y - h - gap : viewport.h - 8 - h
|
|
111
|
+
x = Math.max(8, x)
|
|
112
|
+
y = Math.max(8, y)
|
|
113
|
+
position(node, { x, y, w, h })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createPaintContext() {
|
|
117
|
+
const strata = []
|
|
118
|
+
const hitList = []
|
|
119
|
+
const scrolls = []
|
|
120
|
+
const ctx = {
|
|
121
|
+
strata,
|
|
122
|
+
hitList,
|
|
123
|
+
scrolls,
|
|
124
|
+
stratum: 0,
|
|
125
|
+
clip: null,
|
|
126
|
+
glass: null,
|
|
127
|
+
theme: CONTENT_THEME,
|
|
128
|
+
order: 0,
|
|
129
|
+
frame,
|
|
130
|
+
probeFor: (index) => probe.current(index),
|
|
131
|
+
glassOps: [],
|
|
132
|
+
stratumFor(bounds, base) {
|
|
133
|
+
let stratum = base
|
|
134
|
+
for (const other of ctx.glassOps) {
|
|
135
|
+
if (other.stratum >= stratum && rectsOverlap(other.bounds, bounds)) stratum = other.stratum + 1
|
|
136
|
+
}
|
|
137
|
+
return stratum
|
|
138
|
+
},
|
|
139
|
+
ensure(index) {
|
|
140
|
+
while (strata.length <= index) strata.push({ ops: [], glass: [] })
|
|
141
|
+
return strata[index]
|
|
142
|
+
},
|
|
143
|
+
fade: null,
|
|
144
|
+
opacity: 1,
|
|
145
|
+
emit(op) {
|
|
146
|
+
op.clip = ctx.clip
|
|
147
|
+
op.fade = ctx.fade
|
|
148
|
+
if (ctx.opacity < 1 && op.color) op.color = [op.color[0], op.color[1], op.color[2], op.color[3] * ctx.opacity]
|
|
149
|
+
ctx.ensure(ctx.stratum).ops.push(op)
|
|
150
|
+
return op
|
|
151
|
+
},
|
|
152
|
+
emitGlass(op) {
|
|
153
|
+
op.clip = ctx.clip
|
|
154
|
+
op.stratum = ctx.stratum
|
|
155
|
+
ctx.ensure(ctx.stratum).glass.push(op)
|
|
156
|
+
ctx.glassOps.push(op)
|
|
157
|
+
return op
|
|
158
|
+
},
|
|
159
|
+
hit(node, rect) {
|
|
160
|
+
hitList.push({ id: node.id, rect: rect ?? node.rect, clip: ctx.clip, layer: node.layer, order: ctx.order++ })
|
|
161
|
+
},
|
|
162
|
+
scroll(node) {
|
|
163
|
+
scrolls.push({ id: node.id, rect: node.rect, clip: ctx.clip, horizontal: Boolean(node.props.horizontal) })
|
|
164
|
+
},
|
|
165
|
+
pushClip(rect) {
|
|
166
|
+
const previous = ctx.clip
|
|
167
|
+
ctx.clip = previous ? rectIntersect(previous, rect) : { ...rect }
|
|
168
|
+
return previous
|
|
169
|
+
},
|
|
170
|
+
popClip(previous) {
|
|
171
|
+
ctx.clip = previous
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
return ctx
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function paintNode(node, ctx) {
|
|
178
|
+
const opacity = node.props.opacity
|
|
179
|
+
const previous = ctx.opacity
|
|
180
|
+
if (opacity != null) ctx.opacity = previous * Math.max(0, Math.min(1, opacity))
|
|
181
|
+
if (opacity != null && ctx.opacity <= 0.001) {
|
|
182
|
+
ctx.opacity = previous
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
if (node.paint) node.paint(node, ctx)
|
|
186
|
+
else paintChildren(node, ctx)
|
|
187
|
+
ctx.opacity = previous
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function paintChildren(node, ctx) {
|
|
191
|
+
for (const child of node.children) {
|
|
192
|
+
if (child.props.detached) continue
|
|
193
|
+
paintNode(child, ctx)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function end(options) {
|
|
198
|
+
layout(root, { x: 0, y: 0, w: viewport.w, h: viewport.h }, {})
|
|
199
|
+
for (const popup of popups) placePopup(popup)
|
|
200
|
+
|
|
201
|
+
const ctx = createPaintContext()
|
|
202
|
+
ctx.paintChildren = paintChildren
|
|
203
|
+
ctx.paintNode = paintNode
|
|
204
|
+
paintNode(root, ctx)
|
|
205
|
+
|
|
206
|
+
const byLayer = [...popups].sort((a, b) => a.layer - b.layer)
|
|
207
|
+
for (const popup of byLayer) {
|
|
208
|
+
ctx.stratum = 0
|
|
209
|
+
ctx.clip = null
|
|
210
|
+
ctx.glass = null
|
|
211
|
+
ctx.theme = CONTENT_THEME
|
|
212
|
+
paintNode(popup, ctx)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const [id, ghost] of ghosts) {
|
|
216
|
+
if (ghost.seen) {
|
|
217
|
+
ghost.seen = false
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
ctx.stratum = 0
|
|
221
|
+
ctx.clip = null
|
|
222
|
+
ctx.glass = null
|
|
223
|
+
ctx.theme = CONTENT_THEME
|
|
224
|
+
if (ghost.paint(ctx)) ghosts.delete(id)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (ctx.strata.length === 0) ctx.ensure(0)
|
|
228
|
+
lastStrata = ctx.strata.map((stratum) => ({ ops: stratum.ops.length, glass: stratum.glass.map((g) => g.id) }))
|
|
229
|
+
renderer.render({ strata: ctx.strata, clearColor: options?.clearColor })
|
|
230
|
+
input.endFrame(ctx.hitList, ctx.scrolls)
|
|
231
|
+
state.nextFrame()
|
|
232
|
+
state.collect()
|
|
233
|
+
return { animating: clock.animating }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function ghost(id, paint) {
|
|
237
|
+
ghosts.set(id, { paint, seen: true })
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function markGhostSeen(id) {
|
|
241
|
+
const entry = ghosts.get(id)
|
|
242
|
+
if (entry) entry.seen = true
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
begin,
|
|
247
|
+
end,
|
|
248
|
+
open,
|
|
249
|
+
close,
|
|
250
|
+
add,
|
|
251
|
+
openPopup,
|
|
252
|
+
current,
|
|
253
|
+
registerFocusable,
|
|
254
|
+
ghost,
|
|
255
|
+
markGhostSeen,
|
|
256
|
+
get focusOrder() {
|
|
257
|
+
return focusOrder
|
|
258
|
+
},
|
|
259
|
+
get frame() {
|
|
260
|
+
return frame
|
|
261
|
+
},
|
|
262
|
+
get viewport() {
|
|
263
|
+
return viewport
|
|
264
|
+
},
|
|
265
|
+
get root() {
|
|
266
|
+
return root
|
|
267
|
+
},
|
|
268
|
+
get lastStrata() {
|
|
269
|
+
return lastStrata
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function rect(x, y, w, h) {
|
|
2
|
+
return { x, y, w, h }
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function rectContains(r, px, py) {
|
|
6
|
+
return px >= r.x && py >= r.y && px < r.x + r.w && py < r.y + r.h
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function rectIntersect(a, b) {
|
|
10
|
+
const x = Math.max(a.x, b.x)
|
|
11
|
+
const y = Math.max(a.y, b.y)
|
|
12
|
+
const r = Math.min(a.x + a.w, b.x + b.w)
|
|
13
|
+
const btm = Math.min(a.y + a.h, b.y + b.h)
|
|
14
|
+
return { x, y, w: Math.max(0, r - x), h: Math.max(0, btm - y) }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function rectUnion(a, b) {
|
|
18
|
+
if (!a) return { ...b }
|
|
19
|
+
if (!b) return { ...a }
|
|
20
|
+
const x = Math.min(a.x, b.x)
|
|
21
|
+
const y = Math.min(a.y, b.y)
|
|
22
|
+
const r = Math.max(a.x + a.w, b.x + b.w)
|
|
23
|
+
const btm = Math.max(a.y + a.h, b.y + b.h)
|
|
24
|
+
return { x, y, w: r - x, h: btm - y }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function rectInset(r, amount) {
|
|
28
|
+
return { x: r.x + amount, y: r.y + amount, w: r.w - amount * 2, h: r.h - amount * 2 }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function rectCenter(r) {
|
|
32
|
+
return { x: r.x + r.w / 2, y: r.y + r.h / 2 }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function rectLerp(a, b, t) {
|
|
36
|
+
return {
|
|
37
|
+
x: a.x + (b.x - a.x) * t,
|
|
38
|
+
y: a.y + (b.y - a.y) * t,
|
|
39
|
+
w: a.w + (b.w - a.w) * t,
|
|
40
|
+
h: a.h + (b.h - a.h) * t,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function rectEquals(a, b) {
|
|
45
|
+
return a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function clamp(v, lo, hi) {
|
|
49
|
+
return v < lo ? lo : v > hi ? hi : v
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function lerp(a, b, t) {
|
|
53
|
+
return a + (b - a) * t
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function padding(value) {
|
|
57
|
+
if (value == null) return [0, 0, 0, 0]
|
|
58
|
+
if (typeof value === 'number') return [value, value, value, value]
|
|
59
|
+
if (value.length === 2) return [value[0], value[1], value[0], value[1]]
|
|
60
|
+
return value
|
|
61
|
+
}
|
package/src/core/ids.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function createIdScope() {
|
|
2
|
+
const stack = ['']
|
|
3
|
+
const counters = new Map()
|
|
4
|
+
|
|
5
|
+
function push(key) {
|
|
6
|
+
stack.push(stack[stack.length - 1] + '/' + key)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function pop() {
|
|
10
|
+
if (stack.length === 1) throw new Error('id scope underflow')
|
|
11
|
+
stack.pop()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function current() {
|
|
15
|
+
return stack[stack.length - 1]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function make(key) {
|
|
19
|
+
const id = current() + '/' + key
|
|
20
|
+
const count = counters.get(id) ?? 0
|
|
21
|
+
counters.set(id, count + 1)
|
|
22
|
+
return count === 0 ? id : id + '#' + count
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function reset() {
|
|
26
|
+
stack.length = 1
|
|
27
|
+
counters.clear()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { push, pop, current, make, reset }
|
|
31
|
+
}
|