Mercurial > code > home > repos > light9
changeset 2246:5c269c03863d
WIP device settings page can now load and save ok. Omitted GraphToControls for now
author | drewp@bigasterisk.com |
---|---|
date | Sat, 27 May 2023 01:14:45 -0700 |
parents | a1f6f3139995 |
children | f5e4aa36985d |
files | light9/live/Effect.ts light9/live/Light9AttrControl.ts light9/live/Light9DeviceControl.ts light9/live/Light9DeviceSettings.ts light9/live/Light9LiveControl.ts light9/live/Light9LiveControls.ts light9/live/index.html light9/web/light9-color-picker.ts |
diffstat | 8 files changed, 507 insertions(+), 399 deletions(-) [+] |
line wrap: on
line diff
--- a/light9/live/Effect.ts Fri May 26 23:07:40 2023 -0700 +++ b/light9/live/Effect.ts Sat May 27 01:14:45 2023 -0700 @@ -4,6 +4,7 @@ import { Patch, patchContainsPreds, patchUpdate } from "../web/patch"; import { SyncedGraph } from "../web/SyncedGraph"; import { shortShow } from "../web/show_specific"; +import { SubEvent } from "sub-events"; // todo: Align these names with newtypes.py, which uses HexColor and VTUnion. type Color = string; @@ -15,11 +16,13 @@ return typeof x == "object" && x.termType == "NamedNode"; } +// todo: eliminate this. address the scaling when we actually scale +// stuff, instead of making a mess of every setting function valuePred(graph: SyncedGraph, attr: NamedNode): NamedNode { const U = graph.U(); const scaledAttributeTypes = [U(":color"), U(":brightness"), U(":uv")]; if (some(scaledAttributeTypes, (x: NamedNode) => attr.equals(x))) { - return U(":scaledValue"); + return U(":value"); } else { return U(":value"); } @@ -29,11 +32,12 @@ export class Effect { private settings: Array<{ device: NamedNode; deviceAttr: NamedNode; setting: NamedNode; value: ControlValue }> = []; private ctxForEffect: NamedNode + settingsChanged:SubEvent<void>=new SubEvent() constructor( public graph: SyncedGraph, public uri: NamedNode, // called if the graph changes our values and not when the caller uses edit() - private onValuesChanged: (values: void) => void + // private onValuesChanged: (values: void) => void ) { this.ctxForEffect = this.graph.Uri(this.uri.value.replace("light9.bigasterisk.com/effect", `light9.bigasterisk.com/show/${shortShow}/effect`)); graph.runHandler(this.rebuildSettingsFromGraph.bind(this), `effect sync ${uri.value}`); @@ -61,7 +65,7 @@ // return; } - // log("syncFromGraph", this.uri); + log("syncFromGraph", this.uri); // this repeats work- it gathers all settings when really some values changed (and we might even know about them). maybe push the value-fetching into a secnod phase of the run, and have the 1st phase drop out early const newSettings = []; @@ -69,7 +73,7 @@ const seenDevAttrPairs: Set<string> = new Set(); for (let setting of Array.from(this.graph.objects(this.uri, U(":setting")))) { - // log(` setting ${setting.value}`); + log(` setting ${setting.value}`); if (!isUri(setting)) throw new Error(); let value: ControlValue; const device = this.graph.uriValue(setting, U(":device")); @@ -92,9 +96,11 @@ newSettings.push({ device, deviceAttr, setting, value }); } + log(newSettings) this.settings = newSettings; log(`rebuild to ${this.settings.length}`); - this.onValuesChanged(); + this.settingsChanged.emit() + // this.onValuesChanged(); } currentValue(device: NamedNode, deviceAttr: NamedNode): ControlValue | null {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/live/Light9AttrControl.ts Sat May 27 01:14:45 2023 -0700 @@ -0,0 +1,260 @@ +import debug from "debug"; +import { css, html, LitElement, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { Literal, NamedNode } from "n3"; +import { SubEvent } from "sub-events"; +import { SyncedGraph } from "../web/SyncedGraph"; +import { ControlValue, Effect } from "./Effect"; +import { GraphToControls } from "./GraphToControls"; +import { DeviceAttrRow } from "./Light9DeviceControl"; +import { Choice } from "./Light9Listbox"; +import { getTopGraph } from "../web/RdfdbSyncedGraph"; +export { Slider } from "@material/mwc-slider"; +export { Light9ColorPicker } from "../web/light9-color-picker"; + +const log = debug("settings.dev.attr"); + +type DataTypeNames = "scalar" | "color" | "choice"; +const makeType = (d: DataTypeNames) => new NamedNode(`http://light9.bigasterisk.com/${d}`); + +// UI for one device attr (of any type). +@customElement("light9-attr-control") +export class Light9AttrControl extends LitElement { + graph!: SyncedGraph; + + static styles = [ + css` + #colorControls { + display: flex; + align-items: center; + } + #colorControls > * { + margin: 0 3px; + } + :host { + border: 2px solid white; + } + mwc-slider { + width: 250px; + } + `, + ]; + + @property() deviceAttrRow: DeviceAttrRow | null = null; + @state() dataType: DataTypeNames = "scalar"; + + @property() effect: Effect | null = null; + // we'll connect to this and receive graphValueChanged and send uiValueChanged + // @property() graphToControls!: GraphToControls; + + @property() enableChange: boolean = false; + @property() value: ControlValue | null = null; // e.g. color string + + // slider mode + // @property() sliderValue: number = 0; + + // color mode + + // choice mode + // @property() pickedChoice: Choice | null = null; + // @property() choiceValue: Choice | null = null; + + valueChanged: SubEvent<Literal> = new SubEvent(); + + constructor() { + super(); + getTopGraph().then((g) => { + this.graph = g; + if (this.deviceAttrRow === null) throw new Error(); + // this.graph.runHandler(this.graphReads.bind(this), `${this.deviceAttrRow.device} ${this.deviceAttrRow.uri} reads`); + }); + } + + connectedCallback(): void { + super.connectedCallback(); + } + + render() { + if (this.deviceAttrRow === null) throw new Error(); + const dbg = html` live-control ${this.dataType} ]`; + if (this.dataType == "scalar") { + const v = this.value || 0; + return html`${dbg} <mwc-slider .value=${v} step=${1 / 255} min="0" max="1" @input=${this.onSliderInput}></mwc-slider> `; + } else if ((this.dataType = "color")) { + const v = this.value || '#000' + return html` + ${dbg} + <div id="colorControls"> + <button on-click="goBlack">0.0</button> + <light9-color-picker .color=${v} @input=${this.onColorInput}></light9-color-picker> + </div> + `; + } else if (this.dataType == "choice") { + return html`${dbg} <light9-listbox .choices=${this.deviceAttrRow.choices} .value=${this.choiceValue}> </light9-listbox> `; + } + } + + // graphReads() { + // if (this.deviceAttrRow === null) throw new Error(); + // const U = this.graph.U(); + // this.effect?.currentValue(this.deviceAttrRow.device, this.deviceAttrRow.uri); + // } + + updated(changedProperties: PropertyValues<this>) { + super.updated(changedProperties); + // if (changedProperties.has("graphToControls")) { + // // this.graphToControls.register(this.device, this.deviceAttrRow.uri, this.onGraphValueChanged.bind(this)); + // this.enableChange = true; + // } + + if (changedProperties.has("deviceAttrRow")) { + this.onDeviceAttrRowProperty(); + } + if (changedProperties.has("effect")) { + this.onEffectProperty(); + } + if (changedProperties.has("value")) { + this.onValueProperty(); + } + } + + private onValueProperty() { + if (this.deviceAttrRow === null) throw new Error(); + if (this.effect !== null && this.graph !== undefined) { + const p = this.effect.edit(this.deviceAttrRow.device, this.deviceAttrRow.uri, this.value); + log("patch", p, "to", this.graph); + if (p.adds.length || p.dels.length) { + this.graph.applyAndSendPatch(p); + } + } + } + + private onEffectProperty() { + if (this.effect === null) throw new Error(); + // effect will read graph changes on its own, but emit an event when it does + this.effect.settingsChanged.subscribe(() => { + this.effectSettingsChanged(); + }); + this.effectSettingsChanged(); + } + + private effectSettingsChanged() { + // anything in the settings graph is new + log("i check the effect current value"); + if (this.deviceAttrRow === null) throw new Error(); + if (this.effect === null) throw new Error(); + log("graph->ui on ", this.deviceAttrRow.device, this.deviceAttrRow.uri); + const v=this.effect.currentValue(this.deviceAttrRow.device, this.deviceAttrRow.uri); + this.onGraphValueChanged(v); + } + + private onDeviceAttrRowProperty() { + if (this.deviceAttrRow === null) throw new Error(); + const d = this.deviceAttrRow.dataType; + if (d.equals(makeType("scalar"))) { + this.dataType = "scalar"; + } else if (d.equals(makeType("color"))) { + this.dataType = "color"; + } else if (d.equals(makeType("choice"))) { + this.dataType = "choice"; + } + } + + onSliderInput(ev: CustomEvent) { + if (ev.detail === undefined) { + // not sure what this is, but it seems to be followed by good events + return; + } + log(ev.type, ev.detail.value); + this.value = ev.detail.value; + // this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, ev.detail.value); + } + +onColorInput(ev: CustomEvent) { + this.value = ev.detail.value; +} + + onGraphValueChanged(v: ControlValue | null) { + if (this.deviceAttrRow === null) throw new Error(); + log("change: control must display", v, "for", this.deviceAttrRow.device.value, this.deviceAttrRow.uri.value); + // this.enableChange = false; + if (this.dataType == "scalar") { + if (v !== null) { + setTimeout(() => { + // only needed once per page layout + this.shadowRoot?.querySelector("mwc-slider")?.layout(/*skipUpdateUI=*/ false); + }, 1); + this.value = v; + } else { + this.value = 0; + } + } else if (this.dataType == "color") { + log('graph sets coolor', v) + this.value=v; + } + // if (v === null) { + // this.clear(); + // } else { + // this.value = v; + // } + // if (this.deviceAttrRow.useChoice) { + // this.choiceValue = v === null ? v : v.value; + // } + // this.enableChange = true; + } + + graphToColor(v: ControlValue | null) { + this.value = v === null ? "#000" : v; + return; + // const cp = this.shadowRoot?.querySelector("light9-color-picker") as Light9ColorPicker | null; + // if (cp) { + // if (typeof v != "string") throw new Error("type v is " + typeof v); + // if (v === null) { + // v = "#000"; + // } + // cp.setColor(v as string); + // } + } + + goBlack() { + this.value = "#000000"; + } + + onChoice(value: any) { + // if (this.graphToControls == null || !this.enableChange) { + // return; + // } + // if (value != null) { + // value = this.graph.Uri(value); + // } else { + // value = null; + // } + // this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value); + } + + onChange(value: any) { + // if (this.graphToControls == null || !this.enableChange) { + // return; + // } + // if (typeof value === "number" && isNaN(value)) { + // return; + // } // let onChoice do it + // //log('change: control tells graph', @deviceAttrRow.uri.value, value) + // if (value === undefined) { + // value = null; + // } + // this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value); + } + + // clear() { + // this.pickedChoice = null; + // this.sliderWriteValue = 0; + // if (this.deviceAttrRow.useColor) { + // return (this.value = "#000000"); + // } else if (this.deviceAttrRow.useChoice) { + // return (this.value = this.pickedChoice = null); + // } else { + // return (this.value = this.sliderValue = 0); + // } + // } +}
--- a/light9/live/Light9DeviceControl.ts Fri May 26 23:07:40 2023 -0700 +++ b/light9/live/Light9DeviceControl.ts Sat May 27 01:14:45 2023 -0700 @@ -8,13 +8,15 @@ import { SyncedGraph } from "../web/SyncedGraph"; import { GraphToControls } from "./GraphToControls"; import { Choice } from "./Light9Listbox"; -import { Light9LiveControl } from "./Light9LiveControl"; +import { Light9AttrControl } from "./Light9AttrControl"; +import { Effect } from "./Effect"; export { ResourceDisplay } from "../web/ResourceDisplay"; -export { Light9LiveControl }; -const log = debug("devcontrol"); +export { Light9AttrControl }; +const log = debug("settings.dev"); export interface DeviceAttrRow { uri: NamedNode; //devattr + device: NamedNode; attrClasses: string; // the css kind dataType: NamedNode; showColorPicker: boolean; @@ -26,6 +28,7 @@ max: number; } +// Widgets for one device with multiple Light9LiveControl rows for the attr(s). @customElement("light9-device-control") export class Light9DeviceControl extends LitElement { graph!: SyncedGraph; @@ -88,13 +91,13 @@ ${this.deviceAttrs.map( (dattr: DeviceAttrRow) => html` <div @click="onAttrClick" class="deviceAttr ${dattr.attrClasses}"> - <span>attr <resource-display minor .uri="${dattr.uri}"></resource-display></span> - <light9-live-control - .device="${this.uri}" - .deviceAttrRow="${dattr}" - .effect="${this.effect}" - .graphToControls="${this.graphToControls}" - ></light9-live-control> + <span> + attr + <resource-display minor .uri=${dattr.uri}></resource-display> + </span> + <light9-attr-control .deviceAttrRow=${dattr} .effect=${this.effect}> + .graphToControls={this.graphToControls} + </light9-attr-control> </div> ` )} @@ -103,8 +106,8 @@ } @property() uri!: NamedNode; - @property() effect!: NamedNode; - @property() graphToControls!: GraphToControls; + @property() effect!: Effect; + // @property() graphToControls!: GraphToControls; @property() devClasses: string = ""; // the css kind @property() deviceAttrs: DeviceAttrRow[] = []; @@ -151,9 +154,9 @@ return; } try { - this.deviceClass = this.graph.uriValue(this.uri, U("rdf:type")); - } catch(e) { - // what's likely is we're going through a graph reload and the graph + this.deviceClass = this.graph.uriValue(this.uri, U("rdf:type")); + } catch (e) { + // what's likely is we're going through a graph reload and the graph // is gone but the controls remain } this.deviceAttrs = []; @@ -169,6 +172,7 @@ const dataType = this.graph.uriValue(devAttr, U(":dataType")); const daRow = { uri: devAttr, + device: this.uri, dataType, showColorPicker: dataType.equals(U(":color")), attrClasses: this.selectedAttrs.has(devAttr) ? "selected" : "", @@ -206,8 +210,8 @@ clear() { // why can't we just set their values ? what's diff about // the clear state, and should it be represented with `null` value? - throw new Error() - Array.from(this.shadowRoot!.querySelectorAll("light9-live-control")).map((lc: Element) => (lc as Light9LiveControl).clear()); + throw new Error(); + // Array.from(this.shadowRoot!.querySelectorAll("light9-live-control")).map((lc: Element) => (lc as Light9LiveControl).clear()); } onClick(ev: any) {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/live/Light9DeviceSettings.ts Sat May 27 01:14:45 2023 -0700 @@ -0,0 +1,208 @@ +import debug from "debug"; +import { css, html, LitElement, PropertyValues } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { NamedNode } from "n3"; +import { sortBy, uniq } from "underscore"; +import { Patch, patchContainsPreds } from "../web/patch"; +import { getTopGraph } from "../web/RdfdbSyncedGraph"; +import { SyncedGraph } from "../web/SyncedGraph"; +import { GraphToControls } from "./GraphToControls"; +import { Effect } from "./Effect"; +export { EditChoice } from "../web/EditChoice"; +export { Light9DeviceControl as Light9LiveDeviceControl } from "./Light9DeviceControl"; +const log = debug("settings"); + +@customElement("light9-device-settings") +export class Light9DeviceSettings extends LitElement { + graph!: SyncedGraph; + + static styles = [ + css` + :host { + display: flex; + flex-direction: column; + } + #preview { + width: 100%; + } + #deviceControls { + flex-grow: 1; + position: relative; + width: 100%; + overflow-y: auto; + } + + light9-live-device-control > div { + break-inside: avoid-column; + } + light9-live-device-control { + } + `, + ]; + + render() { + return html` + <rdfdb-synced-graph></rdfdb-synced-graph> + + <h1>effect EeviceSettings</h1> + + <div id="save"> + <div> + <button @click=${this.newEffect}>New effect</button> + <edit-choice .uri=${this.currentEffect ? this.currentEffect.uri : null} @edited=${this.onEffectChoice2}></edit-choice> + <button @click=${this.clearAll}>clear settings in this effect</button> + </div> + </div> + + <div id="deviceControls"> + ${this.devices.map( + (device: NamedNode) => html` + <light9-device-control .uri=${device} .effect=${this.currentEffect}> .graphToControls={this.graphToControls} </light9-device-control> + ` + )} + </div> + `; + } + + devices: Array<NamedNode> = []; + // uri of the effect being edited, or null. This is the + // master value; GraphToControls follows. + @property() currentEffect: Effect | null = null; + // graphToControls!: GraphToControls; + okToWriteUrl: boolean = false; + + constructor() { + super(); + + getTopGraph().then((g) => { + this.graph = g; + this.graph.runHandler(this.findDevices.bind(this), "findDevices"); + // this.graphToControls = new GraphToControls(this.graph); + // this.graph.runHandler(this.update.bind(this), "Light9LiveControls update"); + this.setEffectFromUrl(); + }); + } + onEffectChoice2(ev: CustomEvent) { + const uri = ev.detail.newValue as NamedNode; + if (uri === null) { + this.currentEffect = null; + } else { + this.currentEffect = new Effect(this.graph, uri); + } + } + updated(changedProperties: PropertyValues<this>) { + log("ctls udpated", changedProperties); + if (changedProperties.has("currentEffect")) { + log(`effectChoice to ${this.currentEffect?.uri?.value}`); + this.onEffectChoice(); + } + // this.graphToControls?.debugDump(); + } + + // Note that this doesn't fetch setting values, so it only should get rerun + // upon (rarer) changes to the devices etc. + findDevices(patch?: Patch) { + const U = this.graph.U(); + // if (patch && !patchContainsPreds(patch, [U("rdf:type")])) { + // return; + // } + + this.devices = []; + let classes = this.graph.subjects(U("rdf:type"), U(":DeviceClass")); + log(`found ${classes.length} device classes`); + uniq(sortBy(classes, "value"), true).forEach((dc) => { + sortBy(this.graph.subjects(U("rdf:type"), dc), "value").forEach((dev) => { + log(`found dev ${dev.value}`); + this.devices.push(dev as NamedNode); + }); + }); + this.requestUpdate(); + } + + setEffectFromUrl() { + // not a continuous bidi link between url and effect; it only reads + // the url when the page loads. + const effect = new URL(window.location.href).searchParams.get("effect"); + if (effect != null) { + log(`found effect in url ${effect}`); + this.currentEffect = new Effect(this.graph, this.graph.Uri(effect)); + } + this.okToWriteUrl = true; + } + + writeToUrl(effect: NamedNode | undefined) { + const effectStr = effect ? this.graph.shorten(effect) : ""; + if (!this.okToWriteUrl) { + return; + } + const u = new URL(window.location.href); + if ((u.searchParams.get("effect") || "") === effectStr) { + return; + } + u.searchParams.set("effect", effectStr); // this escapes : and / and i wish it didn't + window.history.replaceState({}, "", u.href); + log("wrote new url", u.href); + } + + newEffect() { + // this.effectChoice = this.graphToControls.newEffect(); + } + + onEffectChoice() { + const U = (x: any) => this.graph.Uri(x); + // if (this.effectChoice == null) { + // // unlink + // log("onEffectChoice unlink"); + // if (this.graphToControls != null) { + // this.graphToControls.setEffect(null); + // } + // } else { + // if (this.graphToControls != null) { + // this.graphToControls.setEffect(this.effectChoice); + // } else { + // throw new Error("graphToControls not set"); + // } + // } + this.writeToUrl(this.currentEffect?.uri); + } + + clearAll() { + // clears the effect! + return; //this.graphToControls.emptyEffect(); + } + + // configureFromGraph() { + // const U = (x: string) => this.graph.Uri(x); + + // const newDevs: NamedNode[] = []; + // for (let dc of Array.from(this.graph.sortedUris(this.graph.subjects(U("rdf:type"), U(":DeviceClass"))))) { + // for (let dev of Array.from(this.graph.sortedUris(this.graph.subjects(U("rdf:type"), dc)))) { + // if (this.graph.contains(dev, U(":hideInLiveUi"), null)) { + // continue; + // } + // if (newDevs.length == 0) newDevs.push(dev); + // } + // } + // log("is this called?"); + // log(`controls update now has ${newDevs.length} devices`); + // this.devices = newDevs; + // this.requestUpdate(); + + // return; + + // // Tried css columns- big slowdown from relayout as I'm scrolling. + // // Tried isotope- seems to only scroll to the right. + // // Tried columnize- fails in jquery maybe from weird elements. + + // // not sure how to get this run after the children are created + // return setTimeout( + // () => + // $("#deviceControls").isotope({ + // // fitColumns would be nice, but it doesn't scroll vertically + // layoutMode: "masonry", + // containerStyle: null, + // }), + // 2000 + // ); + // } +}
--- a/light9/live/Light9LiveControl.ts Fri May 26 23:07:40 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,172 +0,0 @@ -import debug from "debug"; -import { css, html, LitElement, PropertyValues } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { Literal, NamedNode } from "n3"; -import { SubEvent } from "sub-events"; -import { SyncedGraph } from "../web/SyncedGraph"; -import { ControlValue } from "./Effect"; -import { GraphToControls } from "./GraphToControls"; -import { DeviceAttrRow } from "./Light9DeviceControl"; -import { Choice } from "./Light9Listbox"; -export { Slider } from "@material/mwc-slider"; -export { Light9ColorPicker } from "../web/light9-color-picker"; - -const log = debug("control"); - - -const makeType = (d: "scalar" | "color" | "choice") => new NamedNode(`http://light9.bigasterisk.com/${d}`); - -// UI for one device attr (of any type). -@customElement("light9-live-control") -export class Light9LiveControl extends LitElement { - graph!: SyncedGraph; - - static styles = [ - css` - #colorControls { - display: flex; - align-items: center; - } - #colorControls > * { - margin: 0 3px; - } - :host { - border: 2px solid white; - } - `, - ]; - - // passed from parent - @property() device!: NamedNode; - @property() dataType: NamedNode; - @property() deviceAttrRow!: DeviceAttrRow; - // we'll connect to this and receive graphValueChanged and send uiValueChanged - @property() graphToControls!: GraphToControls; - - @property() enableChange: boolean = false; - @property() value: ControlValue | null = null; - - // slider mode - @property() sliderValue: number = 0; - - // color mode - - // choice mode - @property() pickedChoice: Choice | null = null; - @property() choiceValue: Choice | null = null; - - valueChanged: SubEvent<Literal> = new SubEvent(); - - constructor() { - super(); - this.dataType = makeType("color"); - // getTopGraph().then((g) => { - // this.graph = g; - // // this.graph.runHandler(this.graphReads.bind(this), `${this.device} ${this.deviceAttrRow.uri} reads`); - // }); - } - - render() { - const dbg=html` - ]` - if (this.dataType.equals(makeType("scalar"))) { - return html`${dbg} <mwc-slider .value=${this.sliderValue} step=${1 / 255} min="0" max="1" @input=${this.onSliderInput}></mwc-slider> `; - } else if (this.dataType.equals(makeType("color"))) { - return html` ${dbg} - <div id="colorControls"> - <button on-click="goBlack">0.0</button> - <light9-color-picker color="${this.value}"></light9-color-picker> - </div> - `; - } else if (this.dataType.equals(makeType("choice"))) { - return html`${dbg} <light9-listbox choices="{{deviceAttrRow.choices}}" value="{{choiceValue}}"> </light9-listbox> `; - } - } - - // graphReads() { - // const U = this.graph.U(); - // } - - updated(changedProperties: PropertyValues) { - if (changedProperties.has("graphToControls")) { - this.graphToControls.register(this.device, this.deviceAttrRow.uri, this.onGraphValueChanged.bind(this)); - this.enableChange = true; - } - } - - onSliderInput(ev: CustomEvent) { - if (ev.detail === undefined) { - // not sure what this is, but it seems to be followed by good events - return; - } - log(ev.type, ev.detail?.value); - this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, ev.detail.value); - } - - onGraphValueChanged(v: ControlValue | null) { - // log("change: control must display", v); - // this.enableChange = false; - if (this.dataType.equals(makeType("scalar"))) { - if (v !== null) { - setTimeout(() => { - // only needed once per page layout - this.shadowRoot?.querySelector("mwc-slider")?.layout(/*skipUpdateUI=*/ false); - }, 1); - this.sliderValue = v as number; - } else { - this.sliderValue = 0; - } - } - // if (v === null) { - // this.clear(); - // } else { - // this.value = v; - // } - // if (this.deviceAttrRow.useChoice) { - // this.choiceValue = v === null ? v : v.value; - // } - // this.enableChange = true; - } - - goBlack() { - this.value = "#000000"; - } - - onChoice(value: any) { - if (this.graphToControls == null || !this.enableChange) { - return; - } - if (value != null) { - value = this.graph.Uri(value); - } else { - value = null; - } - this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value); - } - - onChange(value: any) { - if (this.graphToControls == null || !this.enableChange) { - return; - } - if (typeof value === "number" && isNaN(value)) { - return; - } // let onChoice do it - //log('change: control tells graph', @deviceAttrRow.uri.value, value) - if (value === undefined) { - value = null; - } - this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value); - } - - // clear() { - // this.pickedChoice = null; - // this.sliderWriteValue = 0; - // if (this.deviceAttrRow.useColor) { - // return (this.value = "#000000"); - // } else if (this.deviceAttrRow.useChoice) { - // return (this.value = this.pickedChoice = null); - // } else { - // return (this.value = this.sliderValue = 0); - // } - // } -}
--- a/light9/live/Light9LiveControls.ts Fri May 26 23:07:40 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,200 +0,0 @@ -import debug from "debug"; -import { css, html, LitElement, PropertyValues } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { NamedNode } from "n3"; -import { sortBy, uniq } from "underscore"; -import { Patch, patchContainsPreds } from "../web/patch"; -import { getTopGraph } from "../web/RdfdbSyncedGraph"; -import { SyncedGraph } from "../web/SyncedGraph"; -import { GraphToControls } from "./GraphToControls"; -export { EditChoice } from "../web/EditChoice"; -export { Light9DeviceControl as Light9LiveDeviceControl } from "./Light9DeviceControl"; -const log = debug("controls"); - -@customElement("light9-live-controls") -export class Light9LiveControls extends LitElement { - graph!: SyncedGraph; - - static styles = [ - css` - :host { - display: flex; - flex-direction: column; - } - #preview { - width: 100%; - } - #deviceControls { - flex-grow: 1; - position: relative; - width: 100%; - overflow-y: auto; - } - - light9-live-device-control > div { - break-inside: avoid-column; - } - light9-live-device-control { - } - `, - ]; - - render() { - return html` - <rdfdb-synced-graph></rdfdb-synced-graph> - - <h1>effect deviceattrs</h1> - - <div id="save"> - <div> - <button @click=${this.newEffect}>New effect</button> - <edit-choice .uri=${this.effectChoice} @edited=${this.onEffectChoice2}></edit-choice> - <button @click=${this.clearAll}>clear settings in this effect</button> - </div> - </div> - - <div id="deviceControls"> - ${this.devices.map( - (device: NamedNode) => html` - <light9-device-control .uri=${device} .effect=${this.effectChoice} .graphToControls=${this.graphToControls}></light9-device-control> - ` - )} - </div> - `; - } - - devices: Array<NamedNode> = []; - // uri of the effect being edited, or null. This is the - // master value; GraphToControls follows. - @property() effectChoice: NamedNode | null = null; - graphToControls!: GraphToControls; - okToWriteUrl: boolean = false; - - constructor() { - super(); - - getTopGraph().then((g) => { - this.graph = g; - this.graph.runHandler(this.findDevices.bind(this), "findDevices"); - this.graphToControls = new GraphToControls(this.graph); - // this.graph.runHandler(this.update.bind(this), "Light9LiveControls update"); - this.setEffectFromUrl(); - }); - } - onEffectChoice2(ev: CustomEvent) { - this.effectChoice = ev.detail.newValue as NamedNode; - } - updated(changedProperties: PropertyValues) { - if (changedProperties.has("effectChoice")) { - log(`effectChoice to ${this.effectChoice?.value}`); - this.onEffectChoice(); - } - } - - // Note that this doesn't fetch setting values, so it only should get rerun - // upon (rarer) changes to the devices etc. - findDevices(patch?: Patch) { - const U = this.graph.U(); - // if (patch && !patchContainsPreds(patch, [U("rdf:type")])) { - // return; - // } - - this.devices = []; - let classes = this.graph.subjects(U("rdf:type"), U(":DeviceClass")); - log(`found ${classes.length} device classes`); - uniq(sortBy(classes, "value"), true).forEach((dc) => { - sortBy(this.graph.subjects(U("rdf:type"), dc), "value").forEach((dev) => { - log(`found dev ${dev.value}`); - this.devices.push(dev as NamedNode); - }); - }); - this.requestUpdate(); - } - - setEffectFromUrl() { - // not a continuous bidi link between url and effect; it only reads - // the url when the page loads. - const effect = new URL(window.location.href).searchParams.get("effect"); - if (effect != null) { - log(`found effect in url ${effect}`); - this.effectChoice = this.graph.Uri(effect); - } - this.okToWriteUrl = true; - } - - writeToUrl(effect: NamedNode | null) { - const effectStr = effect ? this.graph.shorten(effect) : ""; - if (!this.okToWriteUrl) { - return; - } - const u = new URL(window.location.href); - if ((u.searchParams.get("effect") || "") === effectStr) { - return; - } - u.searchParams.set("effect", effectStr); // this escapes : and / and i wish it didn't - window.history.replaceState({}, "", u.href); - log("wrote new url", u.href); - } - - newEffect() { - this.effectChoice = this.graphToControls.newEffect(); - } - - onEffectChoice() { - const U = (x: any) => this.graph.Uri(x); - if (this.effectChoice == null) { - // unlink - log("onEffectChoice unlink"); - if (this.graphToControls != null) { - this.graphToControls.setEffect(null); - } - } else { - if (this.graphToControls != null) { - this.graphToControls.setEffect(this.effectChoice); - } else { - throw new Error("graphToControls not set"); - } - } - this.writeToUrl(this.effectChoice); - } - - clearAll() { - // clears the effect! - return this.graphToControls.emptyEffect(); - } - - // configureFromGraph() { - // const U = (x: string) => this.graph.Uri(x); - - // const newDevs: NamedNode[] = []; - // for (let dc of Array.from(this.graph.sortedUris(this.graph.subjects(U("rdf:type"), U(":DeviceClass"))))) { - // for (let dev of Array.from(this.graph.sortedUris(this.graph.subjects(U("rdf:type"), dc)))) { - // if (this.graph.contains(dev, U(":hideInLiveUi"), null)) { - // continue; - // } - // if (newDevs.length == 0) newDevs.push(dev); - // } - // } - // log("is this called?"); - // log(`controls update now has ${newDevs.length} devices`); - // this.devices = newDevs; - // this.requestUpdate(); - - // return; - - // // Tried css columns- big slowdown from relayout as I'm scrolling. - // // Tried isotope- seems to only scroll to the right. - // // Tried columnize- fails in jquery maybe from weird elements. - - // // not sure how to get this run after the children are created - // return setTimeout( - // () => - // $("#deviceControls").isotope({ - // // fitColumns would be nice, but it doesn't scroll vertically - // layoutMode: "masonry", - // containerStyle: null, - // }), - // 2000 - // ); - // } -}
--- a/light9/live/index.html Fri May 26 23:07:40 2023 -0700 +++ b/light9/live/index.html Sat May 27 01:14:45 2023 -0700 @@ -1,10 +1,10 @@ <!DOCTYPE html> <html> <head> - <title>device control</title> + <title>device settings</title> <meta charset="utf-8" /> <link rel="stylesheet" href="../style.css" /> - <script type="module" src="../live/Light9LiveControls"></script> + <script type="module" src="./Light9DeviceSettings"></script> </head> <body> <style> @@ -12,7 +12,7 @@ html { margin: 0; } - light9-live-controls { + light9-device-settings { position: absolute; left: 2px; top: 2px; @@ -20,6 +20,6 @@ bottom: 0; } </style> - <light9-live-controls></light9-live-controls> + <light9-device-settings></light9-device-settings> </body> </html>
--- a/light9/web/light9-color-picker.ts Fri May 26 23:07:40 2023 -0700 +++ b/light9/web/light9-color-picker.ts Sat May 27 01:14:45 2023 -0700 @@ -58,6 +58,7 @@ pickerFloat.pageInit(); } update(changedProperties: PropertyValueMap<this>) { + super.update(changedProperties); if (changedProperties.has("color")) { this.setColor(this.color); } @@ -66,11 +67,12 @@ .value(this.value / 255) .hex(); + this.dispatchEvent(new CustomEvent("input", { detail: { value: this.color } })); + this.swatchEl.then((sw) => { sw.style.borderColor = this.hueSatColor; }); } - super.update(changedProperties); } private onVSliderChange(ev: CustomEvent) {